├── scenarios └── api.github.com │ ├── release-assets │ ├── test-upload.txt │ └── test.js │ ├── get-root │ ├── record.js │ ├── test.js │ ├── normalized-fixture.json │ └── raw-fixture.json │ ├── get-repository │ ├── record.js │ └── test.js │ ├── get-organization │ ├── record.js │ ├── test.js │ ├── normalized-fixture.json │ └── raw-fixture.json │ ├── mark-notifications-as-read │ ├── record.js │ ├── test.js │ ├── normalized-fixture.json │ └── raw-fixture.json │ ├── search-issues │ ├── test.js │ └── record.js │ ├── get-content │ ├── record.js │ ├── test.js │ └── normalized-fixture.json │ ├── markdown │ ├── record.js │ ├── test.js │ ├── normalized-fixture.json │ └── raw-fixture.json │ ├── errors │ ├── test.js │ ├── record.js │ └── normalized-fixture.json │ ├── create-file │ ├── test.js │ ├── record.js │ └── normalized-fixture.json │ ├── lock-issue │ ├── test.js │ ├── record.js │ └── normalized-fixture.json │ ├── paginate-issues │ ├── test.js │ └── record.js │ ├── add-labels-to-issue │ ├── test.js │ └── record.js │ ├── get-archive │ ├── test.js │ ├── record.js │ └── normalized-fixture.json │ ├── create-status │ ├── test.js │ └── record.js │ ├── rename-repository │ ├── test.js │ └── record.js │ ├── labels │ ├── test.js │ └── record.js │ ├── git-refs │ ├── test.js │ └── record.js │ ├── release-assets-conflict │ └── test.js │ ├── add-and-remove-repository-collaborator │ ├── test.js │ └── record.js │ ├── branch-protection │ ├── test.js │ └── record.js │ └── project-cards │ └── test.js ├── .gitignore ├── .github ├── renovate.json ├── workflows │ ├── test.yml │ ├── add_to_octokit_project.yml │ ├── update-prettier.yml │ ├── release.yml │ ├── codeql-analysis.yml │ ├── immediate-response.yml │ └── update.yml ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── bug.yml │ ├── documentation.yml │ ├── maintenance.yml │ └── feature.yml ├── assets └── octokit-fixtures-introduction.png ├── lib ├── env.js ├── calculate-body-length.js ├── normalize │ ├── error.js │ ├── file-change.js │ ├── search-issues.js │ ├── label.js │ ├── user.js │ ├── common.js │ ├── team.js │ ├── project-card-move.js │ ├── reference.js │ ├── organization.js │ ├── status.js │ ├── project-card.js │ ├── invitation.js │ ├── content.js │ ├── release-asset.js │ ├── issue.js │ ├── combined-status.js │ ├── commit.js │ ├── release.js │ ├── archive.js │ ├── branch-protection.js │ └── repository.js ├── set-if-exists.js ├── read.js ├── headers.js ├── remove-credentials.js ├── fixturize-commit-sha.js ├── write.js ├── fixturize-entity-id.js ├── record-scenario.js ├── temporary-repository.js ├── to-entity-name.js └── fixturize-path.js ├── test ├── unit │ ├── fixturize-commit-sha.test.js │ ├── set-if-exists.test.js │ └── temporary-repository.test.js └── integration │ ├── normalize.test.js │ ├── smoke.test.js │ └── additions.test.js ├── SECURITY.md ├── bin └── remove-temporary-repositories.js ├── LICENSE.md ├── package.json ├── CODE_OF_CONDUCT.md ├── index.js ├── README.md └── CONTRIBUTING.md /scenarios/api.github.com/release-assets/test-upload.txt: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .nyc_output 3 | coverage 4 | dist 5 | node_modules 6 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>octokit/.github" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /assets/octokit-fixtures-introduction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octokit/fixtures/HEAD/assets/octokit-fixtures-introduction.png -------------------------------------------------------------------------------- /lib/env.js: -------------------------------------------------------------------------------- 1 | import { cleanEnv, str } from "envalid"; 2 | 3 | export default cleanEnv(process.env, { 4 | FIXTURES_USER_A_TOKEN_FULL_ACCESS: str(), 5 | FIXTURES_USER_B_TOKEN_FULL_ACCESS: str(), 6 | }); 7 | -------------------------------------------------------------------------------- /lib/calculate-body-length.js: -------------------------------------------------------------------------------- 1 | export default calculateBodyLength; 2 | 3 | function calculateBodyLength(body) { 4 | if (typeof body === "string") { 5 | return body.length; 6 | } 7 | 8 | return JSON.stringify(body).length; 9 | } 10 | -------------------------------------------------------------------------------- /lib/normalize/error.js: -------------------------------------------------------------------------------- 1 | export default normalizeContent; 2 | 3 | import setIfExists from "../set-if-exists.js"; 4 | 5 | function normalizeContent(scenarioState, response) { 6 | // zero request ID 7 | setIfExists(response, "request_id", "0000:00000:0000000:0000000:00000000"); 8 | } 9 | -------------------------------------------------------------------------------- /lib/normalize/file-change.js: -------------------------------------------------------------------------------- 1 | export default normalizeFileChange; 2 | 3 | import normalizeCommit from "./commit.js"; 4 | import normalizeContent from "./content.js"; 5 | 6 | function normalizeFileChange(scenarioState, response) { 7 | normalizeCommit(scenarioState, response.commit); 8 | normalizeContent(scenarioState, response.content); 9 | } 10 | -------------------------------------------------------------------------------- /lib/normalize/search-issues.js: -------------------------------------------------------------------------------- 1 | export default normalizeIssuesSearch; 2 | 3 | import normalizeIssue from "./issue.js"; 4 | import setIfExists from "../set-if-exists.js"; 5 | 6 | function normalizeIssuesSearch(scenarioState, { items }) { 7 | items.forEach((result) => { 8 | normalizeIssue(scenarioState, result); 9 | setIfExists(result, "score", 42); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-root/record.js: -------------------------------------------------------------------------------- 1 | import env from "../../../lib/env.js"; 2 | 3 | // https://developer.github.com/v3/#root-endpoint 4 | export default [ 5 | { 6 | method: "get", 7 | url: "/", 8 | headers: { 9 | Accept: "application/vnd.github.v3+json", 10 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-repository/record.js: -------------------------------------------------------------------------------- 1 | import env from "../../../lib/env.js"; 2 | 3 | // https://developer.github.com/v3/repos/#get 4 | export default { 5 | method: "get", 6 | url: "/repos/octokit-fixture-org/hello-world", 7 | headers: { 8 | Accept: "application/vnd.github.v3+json", 9 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-organization/record.js: -------------------------------------------------------------------------------- 1 | import env from "../../../lib/env.js"; 2 | 3 | // https://developer.github.com/v3/orgs/#get-an-organization 4 | export default { 5 | method: "get", 6 | url: "/orgs/octokit-fixture-org", 7 | headers: { 8 | Accept: "application/vnd.github.v3+json", 9 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/unit/fixturize-commit-sha.test.js: -------------------------------------------------------------------------------- 1 | import fixturizeCommitSha from "../../lib/fixturize-commit-sha.js"; 2 | 3 | test("fixturizeCommitSha for fixturized sha", () => { 4 | const map = { 5 | existing: "0000000000000000000000000000000000000001", 6 | }; 7 | const sha = fixturizeCommitSha( 8 | map, 9 | "0000000000000000000000000000000000000001", 10 | ); 11 | expect(sha).toBe("0000000000000000000000000000000000000001"); 12 | }); 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | "on": 3 | push: 4 | branches: 5 | - dependabot/npm_and_yarn/** 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: actions/setup-node@v6 16 | with: 17 | node-version: 24 18 | cache: npm 19 | - run: npm ci 20 | - run: npm test 21 | -------------------------------------------------------------------------------- /lib/set-if-exists.js: -------------------------------------------------------------------------------- 1 | export default setIfExists; 2 | 3 | import get from "lodash/get.js"; 4 | import set from "lodash/set.js"; 5 | 6 | function setIfExists(object, key, value) { 7 | if (!object) { 8 | return; 9 | } 10 | 11 | const currentValue = get(object, key); 12 | 13 | if (currentValue === undefined) { 14 | return; 15 | } 16 | 17 | const newValue = typeof value === "function" ? value(currentValue) : value; 18 | 19 | set(object, key, newValue); 20 | } 21 | -------------------------------------------------------------------------------- /test/unit/set-if-exists.test.js: -------------------------------------------------------------------------------- 1 | import setIfExists from "../../lib/set-if-exists"; 2 | 3 | test("setIfExists accepts undefined as object argument", () => { 4 | const result = setIfExists(undefined, "foo", "bar"); 5 | expect(result).toBe(undefined); 6 | }); 7 | 8 | test("setIfExists sets nested values", () => { 9 | const object = { 10 | foo: { 11 | bar: "baz", 12 | }, 13 | }; 14 | setIfExists(object, "foo.bar", "qux"); 15 | expect(object.foo.bar).toBe("qux"); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/normalize/label.js: -------------------------------------------------------------------------------- 1 | export default normalizeIssue; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import setIfExists from "../set-if-exists.js"; 6 | 7 | function normalizeIssue(scenarioState, response) { 8 | // set all IDs to 1 9 | setIfExists( 10 | response, 11 | "id", 12 | fixturizeEntityId.bind(null, scenarioState.ids, "label"), 13 | ); 14 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 15 | } 16 | -------------------------------------------------------------------------------- /scenarios/api.github.com/mark-notifications-as-read/record.js: -------------------------------------------------------------------------------- 1 | export default markNotificationsAsRead; 2 | import env from "../../../lib/env.js"; 3 | 4 | // https://developer.github.com/v3/activity/notifications/#mark-as-read 5 | async function markNotificationsAsRead(state) { 6 | await state.request({ 7 | method: "put", 8 | url: "/notifications", 9 | headers: { 10 | Accept: "application/vnd.github.v3+json", 11 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 12 | }, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /lib/normalize/user.js: -------------------------------------------------------------------------------- 1 | export default normalizeUser; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import setIfExists from "../set-if-exists.js"; 6 | 7 | function normalizeUser(scenarioState, response) { 8 | // set all IDs to 1 9 | setIfExists( 10 | response, 11 | "id", 12 | fixturizeEntityId.bind(null, scenarioState.ids, "owner"), 13 | ); 14 | 15 | // normalize URLs 16 | setIfExists(response, "avatar_url", fixturizePath.bind(null, scenarioState)); 17 | } 18 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-root/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Get repository", async () => { 6 | const mock = fixtures.mock("api.github.com/get-root"); 7 | 8 | await axios({ 9 | method: "get", 10 | url: "https://api.github.com/", 11 | headers: { 12 | Accept: "application/vnd.github.v3+json", 13 | Authorization: "token 0000000000000000000000000000000000000001", 14 | }, 15 | }).catch(mock.explain); 16 | 17 | expect(mock.done.bind(mock)).not.toThrow(); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/read.js: -------------------------------------------------------------------------------- 1 | export default read; 2 | 3 | import { resolve, dirname } from "path"; 4 | import { fileURLToPath } from "url"; 5 | import { readFile } from "fs/promises"; 6 | 7 | async function read(fixturesPath) { 8 | const path = resolve( 9 | dirname(fileURLToPath(import.meta.url)), 10 | "..", 11 | "scenarios", 12 | fixturesPath, 13 | "normalized-fixture.json", 14 | ); 15 | try { 16 | const json = await readFile(path); 17 | return JSON.parse(json); 18 | } catch (error) { 19 | if (error.code !== "ENOENT") throw error; 20 | 21 | return []; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scenarios/api.github.com/mark-notifications-as-read/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Labels", async () => { 6 | const mock = fixtures.mock("api.github.com/mark-notifications-as-read"); 7 | 8 | await axios({ 9 | method: "put", 10 | url: "https://api.github.com/notifications", 11 | headers: { 12 | Accept: "application/vnd.github.v3+json", 13 | Authorization: "token 0000000000000000000000000000000000000001", 14 | "content-length": 0, 15 | }, 16 | }); 17 | 18 | expect(mock.done.bind(mock)).not.toThrow(); 19 | }); 20 | -------------------------------------------------------------------------------- /.github/workflows/add_to_octokit_project.yml: -------------------------------------------------------------------------------- 1 | name: Add PRs and issues to Octokit org project 2 | 3 | on: 4 | issues: 5 | types: [reopened, opened] 6 | pull_request_target: 7 | types: [reopened, opened] 8 | 9 | jobs: 10 | add-to-project: 11 | name: Add issue to project 12 | runs-on: ubuntu-latest 13 | continue-on-error: true 14 | steps: 15 | - uses: actions/add-to-project@v1.0.2 16 | with: 17 | project-url: https://github.com/orgs/octokit/projects/10 18 | github-token: ${{ secrets.OCTOKITBOT_PROJECT_ACTION_TOKEN }} 19 | labeled: "Status: Stale" 20 | label-operator: NOT 21 | -------------------------------------------------------------------------------- /lib/headers.js: -------------------------------------------------------------------------------- 1 | export default { toObject: rawHeadersToObject, toArray: objectToRawHeaders }; 2 | 3 | function rawHeadersToObject(rawHeaders) { 4 | const keys = []; 5 | const map = {}; 6 | for (let i = 0; i < rawHeaders.length; i = i + 2) { 7 | const key = rawHeaders[i].toLowerCase(); 8 | keys.push(key); 9 | map[key] = rawHeaders[i + 1]; 10 | } 11 | return keys.sort().reduce((headers, key) => { 12 | headers[key] = map[key]; 13 | return headers; 14 | }, {}); 15 | } 16 | 17 | function objectToRawHeaders(map) { 18 | const keys = Object.keys(map).sort(); 19 | return [].concat(...keys.map((key) => [key, map[key]])); 20 | } 21 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-organization/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Get organization", async () => { 6 | const mock = fixtures.mock("api.github.com/get-organization"); 7 | 8 | const result = await axios({ 9 | method: "get", 10 | url: "https://api.github.com/orgs/octokit-fixture-org", 11 | headers: { 12 | Accept: "application/vnd.github.v3+json", 13 | Authorization: "token 0000000000000000000000000000000000000001", 14 | }, 15 | }).catch(mock.explain); 16 | 17 | expect(mock.done.bind(mock)).not.toThrow(); 18 | expect(result.data.id).toBe(1000); 19 | }); 20 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-repository/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Get repository", async () => { 6 | const mock = fixtures.mock("api.github.com/get-repository"); 7 | 8 | const result = await axios({ 9 | method: "get", 10 | url: "https://api.github.com/repos/octokit-fixture-org/hello-world", 11 | headers: { 12 | Accept: "application/vnd.github.v3+json", 13 | Authorization: "token 0000000000000000000000000000000000000001", 14 | }, 15 | }).catch(mock.explain); 16 | 17 | expect(mock.done.bind(mock)).not.toThrow(); 18 | expect(result.data.id).toBe(1000); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/normalize/common.js: -------------------------------------------------------------------------------- 1 | export default normalizeCommon; 2 | 3 | function normalizeCommon(response) { 4 | if (typeof response !== "object") { 5 | return; 6 | } 7 | 8 | traverse({ response }, setNodeId); 9 | } 10 | 11 | function traverse(obj, handle) { 12 | Object.values(obj).forEach((value) => { 13 | if (!value) { 14 | return; 15 | } 16 | 17 | handle(value); 18 | 19 | if (typeof value === "object") { 20 | return traverse(value, handle); 21 | } 22 | }); 23 | } 24 | 25 | function setNodeId(obj) { 26 | if (typeof obj !== "object") { 27 | return; 28 | } 29 | 30 | if ("node_id" in obj) { 31 | obj.node_id = "MDA6RW50aXR5MQ=="; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/remove-credentials.js: -------------------------------------------------------------------------------- 1 | export default removeCredentials; 2 | 3 | import env from "./env.js"; 4 | 5 | const tokenToFixture = { 6 | [env.FIXTURES_USER_A_TOKEN_FULL_ACCESS]: 7 | "0000000000000000000000000000000000000001", 8 | [env.FIXTURES_USER_B_TOKEN_FULL_ACCESS]: 9 | "0000000000000000000000000000000000000002", 10 | }; 11 | 12 | function removeCredentials(fixture) { 13 | // zero auth token 14 | fixture.reqheaders.authorization = ( 15 | fixture.reqheaders.authorization || "" 16 | ).replace(/^token (\w{40})$/, (_, token) => { 17 | token = tokenToFixture[token] || "0000000000000000000000000000000000000000"; 18 | return `token ${token}`; 19 | }); 20 | 21 | return fixture; 22 | } 23 | -------------------------------------------------------------------------------- /scenarios/api.github.com/search-issues/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Labels", async () => { 6 | const mock = fixtures.mock("api.github.com/search-issues"); 7 | 8 | // https://developer.github.com/v3/search/#search-issues 9 | const query = "sesame repo:octokit-fixture-org/search-issues"; 10 | await axios({ 11 | method: "get", 12 | url: `https://api.github.com/search/issues?q=${encodeURIComponent(query)}`, 13 | headers: { 14 | Accept: "application/vnd.github.v3+json", 15 | Authorization: "token 0000000000000000000000000000000000000001", 16 | }, 17 | }).catch(mock.explain); 18 | 19 | expect(mock.done.bind(mock)).not.toThrow(); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/normalize/team.js: -------------------------------------------------------------------------------- 1 | export default normalizeTeam; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import setIfExists from "../set-if-exists.js"; 6 | 7 | function normalizeTeam(scenarioState, response) { 8 | // set all IDs to 1 9 | setIfExists( 10 | response, 11 | "id", 12 | fixturizeEntityId.bind(null, scenarioState.ids, "team"), 13 | ); 14 | 15 | // normalize URLs 16 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 17 | setIfExists(response, "members_url", fixturizePath.bind(null, scenarioState)); 18 | setIfExists( 19 | response, 20 | "repositories_url", 21 | fixturizePath.bind(null, scenarioState), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-content/record.js: -------------------------------------------------------------------------------- 1 | import env from "../../../lib/env.js"; 2 | 3 | // https://developer.github.com/v3/repos/contents/#get-contents 4 | // empty path returns README file if present 5 | export default [ 6 | { 7 | method: "get", 8 | url: "/repos/octokit-fixture-org/hello-world/contents/", 9 | headers: { 10 | accept: "application/vnd.github.v3+json", 11 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 12 | }, 13 | }, 14 | { 15 | method: "get", 16 | url: "/repos/octokit-fixture-org/hello-world/contents/README.md", 17 | headers: { 18 | accept: "application/vnd.github.v3.raw", 19 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /.github/workflows/update-prettier.yml: -------------------------------------------------------------------------------- 1 | name: Update Prettier 2 | "on": 3 | push: 4 | branches: 5 | - renovate/prettier-* 6 | jobs: 7 | update_prettier: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v6 11 | - uses: actions/setup-node@v6 12 | with: 13 | node-version: 24 14 | cache: npm 15 | - run: npm ci 16 | - run: npm run lint:fix 17 | - uses: gr2m/create-or-update-pull-request-action@v1.x 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.OCTOKITBOT_PAT }} 20 | with: 21 | title: Prettier updated 22 | body: An update to prettier required updates to your code. 23 | branch: ${{ github.ref }} 24 | commit-message: "style: prettier" 25 | -------------------------------------------------------------------------------- /lib/normalize/project-card-move.js: -------------------------------------------------------------------------------- 1 | export default projectCardMove; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import setIfExists from "../set-if-exists.js"; 5 | 6 | function projectCardMove(scenarioState, response, fixture) { 7 | setIfExists( 8 | fixture.body, 9 | "column_id", 10 | fixturizeEntityId.bind(null, scenarioState.ids, "project-column"), 11 | ); 12 | 13 | if (/:/.test(fixture.body.position)) { 14 | const originalColumnId = fixture.body.position.split(":")[1]; 15 | const newColumnId = fixturizeEntityId( 16 | scenarioState.ids, 17 | "project-card", 18 | originalColumnId, 19 | ); 20 | 21 | fixture.body.position = fixture.body.position.replace( 22 | /:\d+$/, 23 | `:${newColumnId}`, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/fixturize-commit-sha.js: -------------------------------------------------------------------------------- 1 | export default fixturizeCommitSha; 2 | 3 | // We keep track of commit sha hashes. We don’t want to simply zero them as 4 | // there can be multiple commits. So instead we keep state of a counter and 5 | // pad the counter with 0s left. 6 | function fixturizeCommitSha(map, sha) { 7 | // Do nothing if the passed sha is already set 8 | if (map[sha]) { 9 | return map[sha]; 10 | } 11 | 12 | // Do nothing if the passed sha is one of our fixturized ones 13 | if (Object.values(map).includes(sha)) { 14 | return sha; 15 | } 16 | 17 | // Otherwise calculate the new sha and set it on the passed state map. 18 | const counter = Object.keys(map).length + 1; 19 | map[sha] = ("0000000000000000000000000000000000000000" + counter).substr(-40); 20 | 21 | return map[sha]; 22 | } 23 | -------------------------------------------------------------------------------- /lib/normalize/reference.js: -------------------------------------------------------------------------------- 1 | export default normalizeReference; 2 | import fixturizeCommitSha from "../fixturize-commit-sha.js"; 3 | import fixturizePath from "../fixturize-path.js"; 4 | import setIfExists from "../set-if-exists.js"; 5 | 6 | function normalizeReference(scenarioState, response, fixture) { 7 | // fixturize commit sha hashes 8 | setIfExists( 9 | response, 10 | "object.sha", 11 | fixturizeCommitSha.bind(null, scenarioState.commitSha), 12 | ); 13 | setIfExists( 14 | fixture, 15 | "body.sha", 16 | fixturizeCommitSha.bind(null, scenarioState.commitSha), 17 | ); 18 | 19 | // normalize temporary repository 20 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 21 | setIfExists(response, "object.url", fixturizePath.bind(null, scenarioState)); 22 | } 23 | -------------------------------------------------------------------------------- /lib/normalize/organization.js: -------------------------------------------------------------------------------- 1 | export default normalizeOrganization; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import setIfExists from "../set-if-exists.js"; 6 | 7 | function normalizeOrganization(scenarioState, response) { 8 | // set all IDs to 1 9 | setIfExists( 10 | response, 11 | "id", 12 | fixturizeEntityId.bind(null, scenarioState.ids, "owner"), 13 | ); 14 | setIfExists(response, "avatar_url", fixturizePath.bind(null, scenarioState)); 15 | 16 | // set all dates to Universe 2017 Keynote time 17 | setIfExists(response, "created_at", "2017-10-10T16:00:00Z"); 18 | setIfExists(response, "updated_at", "2017-10-10T16:00:00Z"); 19 | 20 | // set all counts to 42 21 | setIfExists(response, "public_repos", 42); 22 | } 23 | -------------------------------------------------------------------------------- /scenarios/api.github.com/markdown/record.js: -------------------------------------------------------------------------------- 1 | import env from "../../../lib/env.js"; 2 | 3 | // https://developer.github.com/v3/markdown/ 4 | export default [ 5 | { 6 | method: "post", 7 | url: "/markdown", 8 | headers: { 9 | Accept: "text/html", 10 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 11 | }, 12 | data: { 13 | text: `### Hello 14 | 15 | b597b5d`, 16 | context: "octokit-fixture-org/hello-world", 17 | mode: "gfm", 18 | }, 19 | }, 20 | { 21 | method: "post", 22 | url: "/markdown/raw", 23 | headers: { 24 | Accept: "text/html", 25 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 26 | "Content-Type": "text/plain; charset=utf-8", 27 | }, 28 | data: `### Hello 29 | 30 | b597b5d`, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /scenarios/api.github.com/errors/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Errors", async () => { 6 | expect.assertions(2); 7 | const mock = fixtures.mock("api.github.com/errors"); 8 | 9 | try { 10 | await axios({ 11 | method: "post", 12 | url: "https://api.github.com/repos/octokit-fixture-org/errors/labels", 13 | headers: { 14 | Accept: "application/vnd.github.v3+json", 15 | Authorization: "token 0000000000000000000000000000000000000001", 16 | "Content-Type": "application/json; charset=utf-8", 17 | }, 18 | data: { 19 | name: "foo", 20 | color: "invalid", 21 | }, 22 | }); 23 | } catch (error) { 24 | expect(error.response.status).toBe(422); 25 | } 26 | 27 | expect(mock.done.bind(mock)).not.toThrow(); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/normalize/status.js: -------------------------------------------------------------------------------- 1 | export default normalizeStatus; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import normalizeUser from "./user.js"; 6 | import setIfExists from "../set-if-exists.js"; 7 | 8 | function normalizeStatus(scenarioState, response, fixture) { 9 | // set ID to 1 10 | setIfExists( 11 | response, 12 | "id", 13 | fixturizeEntityId.bind(null, scenarioState.ids, "status"), 14 | ); 15 | 16 | // set all dates to Universe 2017 Keynote time 17 | setIfExists(response, "created_at", "2017-10-10T16:00:00Z"); 18 | setIfExists(response, "updated_at", "2017-10-10T16:00:00Z"); 19 | 20 | // normalize temporary repository & fixturize sha 21 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 22 | 23 | normalizeUser(scenarioState, response.creator); 24 | } 25 | -------------------------------------------------------------------------------- /scenarios/api.github.com/create-file/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Create File", async () => { 6 | const mock = fixtures.mock("api.github.com/create-file"); 7 | 8 | const result = await axios({ 9 | method: "put", 10 | url: "https://api.github.com/repos/octokit-fixture-org/create-file/contents/test.txt", 11 | headers: { 12 | Accept: "application/vnd.github.v3+json", 13 | Authorization: "token 0000000000000000000000000000000000000001", 14 | "Content-Type": "application/json; charset=utf-8", 15 | }, 16 | data: { 17 | message: "create test.txt", 18 | content: Buffer.from("Test content").toString("base64"), 19 | }, 20 | }).catch(mock.explain); 21 | 22 | expect(mock.done.bind(mock)).not.toThrow(); 23 | expect(result.data.content.type).toBe("file"); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/write.js: -------------------------------------------------------------------------------- 1 | export default write; 2 | 3 | import { resolve, dirname } from "path"; 4 | import { writeFile } from "fs/promises"; 5 | import { fileURLToPath } from "url"; 6 | 7 | import * as prettier from "prettier"; 8 | import { mkdirp } from "mkdirp"; 9 | 10 | async function write(fixturesPath, fixtures) { 11 | const path = resolve( 12 | dirname(fileURLToPath(import.meta.url)), 13 | "..", 14 | "scenarios", 15 | fixturesPath, 16 | ); 17 | 18 | await mkdirp(dirname(path)); 19 | return Promise.all([ 20 | writeFile( 21 | resolve(path, "normalized-fixture.json"), 22 | await prettier.format(JSON.stringify(fixtures.normalized), { 23 | parser: "json", 24 | }), 25 | ), 26 | writeFile( 27 | resolve(path, "raw-fixture.json"), 28 | await prettier.format(JSON.stringify(fixtures.raw), { parser: "json" }), 29 | ), 30 | ]); 31 | } 32 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thanks for helping make GitHub Open Source Software safe for everyone. 4 | 5 | GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [Octokit](https://github.com/octokit). 6 | 7 | Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we want to make sure that your finding gets passed along to the maintainers of this project for remediation. 8 | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Since this source is part of [Octokit](https://github.com/octokit) (a GitHub organization) we ask that you follow the guidelines [here](https://github.com/github/.github/blob/master/SECURITY.md#reporting-security-issues) to report anything that you might've found. 13 | -------------------------------------------------------------------------------- /bin/remove-temporary-repositories.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import axios from "axios"; 4 | 5 | import env from "../lib/env.js"; 6 | import { regex } from "../lib/temporary-repository.js"; 7 | 8 | const github = axios.create({ 9 | baseURL: "https://api.github.com", 10 | headers: { 11 | Accept: "application/vnd.github.v3+json", 12 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 13 | }, 14 | }); 15 | 16 | github 17 | .get("/orgs/octokit-fixture-org/repos") 18 | 19 | .then((result) => { 20 | return Promise.all( 21 | result.data 22 | .map((repository) => repository.name) 23 | .filter((name) => regex.test(name)) 24 | .map((name) => { 25 | return github 26 | .delete(`/repos/octokit-fixture-org/${name}`) 27 | 28 | .then(() => { 29 | console.log(`✅ ${name} deleted`); 30 | }); 31 | }), 32 | ); 33 | }) 34 | 35 | .catch(console.log); 36 | -------------------------------------------------------------------------------- /lib/fixturize-entity-id.js: -------------------------------------------------------------------------------- 1 | export default fixturizeEntityId; 2 | 3 | // In cases when we can’t simply set IDs to 1 because we have to handle multiple 4 | // entities of the same type, this method returns counter-based IDs per type 5 | function fixturizeEntityId(entityIdsMap, entityName, id) { 6 | if (!entityIdsMap[entityName]) { 7 | entityIdsMap[entityName] = {}; 8 | } 9 | 10 | const map = entityIdsMap[entityName]; 11 | 12 | // Do nothing if the passed id is already set 13 | if (map[id]) { 14 | return map[id]; 15 | } 16 | 17 | // Do nothing if passed id is a normalized one 18 | const check = parseInt(id, 10); 19 | if (Object.values(map).includes(check)) { 20 | return check; 21 | } 22 | 23 | // Otherwise calculate the new id and set it on the passed state map. 24 | // IDs start at 1000 to differentiate from issue/PR numbers 25 | const counter = Object.keys(map).length + 1000; 26 | map[id] = counter; 27 | 28 | // return the new id 29 | return map[id]; 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | "on": 3 | push: 4 | branches: 5 | - main 6 | - next 7 | - beta 8 | - v*.x 9 | # These are recommended by the semantic-release docs: https://github.com/semantic-release/npm#npm-provenance 10 | permissions: 11 | contents: write # to be able to publish a GitHub release 12 | issues: write # to be able to comment on released issues 13 | pull-requests: write # to be able to comment on released pull requests 14 | id-token: write # to enable use of OIDC for npm provenance 15 | 16 | jobs: 17 | release: 18 | name: release 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v6 22 | - uses: actions/setup-node@v6 23 | with: 24 | node-version: lts/* 25 | cache: npm 26 | - run: npm ci 27 | - run: npx semantic-release 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | NPM_TOKEN: ${{ secrets.OCTOKITBOT_NPM_TOKEN }} 31 | -------------------------------------------------------------------------------- /lib/normalize/project-card.js: -------------------------------------------------------------------------------- 1 | export default projectCard; 2 | import fixturizeEntityId from "../fixturize-entity-id.js"; 3 | import fixturizePath from "../fixturize-path.js"; 4 | import normalizeUser from "./user.js"; 5 | import setIfExists from "../set-if-exists.js"; 6 | 7 | function projectCard(scenarioState, response, fixture) { 8 | // set all IDs to 1 9 | setIfExists( 10 | response, 11 | "id", 12 | fixturizeEntityId.bind(null, scenarioState.ids, "project-card"), 13 | ); 14 | 15 | // normalize URLs 16 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 17 | setIfExists(response, "column_url", fixturizePath.bind(null, scenarioState)); 18 | setIfExists(response, "project_url", fixturizePath.bind(null, scenarioState)); 19 | 20 | // set all dates to Universe 2017 Keynote time 21 | setIfExists(response, "created_at", "2017-10-10T16:00:00Z"); 22 | setIfExists(response, "updated_at", "2017-10-10T16:00:00Z"); 23 | 24 | normalizeUser(scenarioState, response.creator); 25 | } 26 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Resolves #ISSUE_NUMBER 4 | 5 | ---- 6 | 7 | ### Before the change? 8 | 9 | 10 | * 11 | 12 | ### After the change? 13 | 14 | 15 | * 16 | 17 | ### Pull request checklist 18 | - [ ] Tests for the changes have been added (for bug fixes / features) 19 | - [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features) 20 | 21 | ### Does this introduce a breaking change? 22 | 23 | 24 | Please see our docs on [breaking changes](https://github.com/octokit/.github/blob/master/community/breaking_changes.md) to help! 25 | 26 | - [ ] Yes 27 | - [ ] No 28 | 29 | ---- 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | "on": 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | schedule: 10 | - cron: 30 23 * * 2 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: 23 | - javascript 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v6 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v3 29 | with: 30 | languages: ${{ matrix.language }} 31 | - name: Autobuild 32 | env: 33 | CODEQL_ACTION_RUN_MODE: Action 34 | uses: >- 35 | github/codeql-action/autobuild@ff3337ee1b38c9bcf43046bde6450e50c5e88ebb 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@v3 38 | -------------------------------------------------------------------------------- /lib/normalize/invitation.js: -------------------------------------------------------------------------------- 1 | export default normalizeInvitation; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import normalizeRepository from "./repository.js"; 6 | import normalizeUser from "./user.js"; 7 | import setIfExists from "../set-if-exists.js"; 8 | 9 | function normalizeInvitation(scenarioState, response) { 10 | // set all IDs to 1 11 | setIfExists( 12 | response, 13 | "id", 14 | fixturizeEntityId.bind(null, scenarioState.ids, "invitation"), 15 | ); 16 | 17 | // set all dates to Universe 2017 Keynote time 18 | setIfExists(response, "created_at", "2017-10-10T09:00:00-07:00"); 19 | 20 | // normalize URLs 21 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 22 | setIfExists(response, "html_url", fixturizePath.bind(null, scenarioState)); 23 | 24 | normalizeRepository(scenarioState, response.repository); 25 | normalizeUser(scenarioState, response.inviter); 26 | normalizeUser(scenarioState, response.invitee); 27 | } 28 | -------------------------------------------------------------------------------- /lib/normalize/content.js: -------------------------------------------------------------------------------- 1 | export default normalizeContent; 2 | 3 | import fixturizePath from "../fixturize-path.js"; 4 | import setIfExists from "../set-if-exists.js"; 5 | 6 | function normalizeContent(scenarioState, response) { 7 | // set all dates to Universe 2017 Keynote time 8 | setIfExists(response, "author.date", "2017-10-10T16:00:00Z"); 9 | setIfExists(response, "committer.date", "2017-10-10T16:00:00Z"); 10 | 11 | // normalize temporary repository 12 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 13 | setIfExists(response, "html_url", fixturizePath.bind(null, scenarioState)); 14 | setIfExists(response, "git_url", fixturizePath.bind(null, scenarioState)); 15 | setIfExists( 16 | response, 17 | "download_url", 18 | fixturizePath.bind(null, scenarioState), 19 | ); 20 | setIfExists(response, "_links.self", fixturizePath.bind(null, scenarioState)); 21 | setIfExists(response, "_links.git", fixturizePath.bind(null, scenarioState)); 22 | setIfExists(response, "_links.html", fixturizePath.bind(null, scenarioState)); 23 | } 24 | -------------------------------------------------------------------------------- /lib/normalize/release-asset.js: -------------------------------------------------------------------------------- 1 | export default normalizeReleaseAsset; 2 | import fixturizeEntityId from "../fixturize-entity-id.js"; 3 | import fixturizePath from "../fixturize-path.js"; 4 | import normalizeUser from "./user.js"; 5 | import setIfExists from "../set-if-exists.js"; 6 | 7 | function normalizeReleaseAsset(scenarioState, response, fixture) { 8 | // set ID to 1 9 | setIfExists( 10 | response, 11 | "id", 12 | fixturizeEntityId.bind(null, scenarioState.ids, "release-asset"), 13 | ); 14 | 15 | // set count to 42 16 | setIfExists(response, "download_count", 42); 17 | 18 | // set all dates to Universe 2017 Keynote time 19 | setIfExists(response, "created_at", "2017-10-10T16:00:00Z"); 20 | setIfExists(response, "updated_at", "2017-10-10T16:00:00Z"); 21 | 22 | // normalize temporary repository name & id in URLs 23 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 24 | setIfExists( 25 | response, 26 | "browser_download_url", 27 | fixturizePath.bind(null, scenarioState), 28 | ); 29 | 30 | normalizeUser(scenarioState, response.uploader); 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/normalize.test.js: -------------------------------------------------------------------------------- 1 | import { globSync } from "glob"; 2 | import { readFileSync } from "fs"; 3 | 4 | import normalize from "../../lib/normalize/index.js"; 5 | 6 | globSync("scenarios/**/raw-fixture.json") 7 | .map((path) => path.replace(/(^scenarios\/|\/raw-fixture.json$)/g, "")) 8 | .forEach((fixtureName) => { 9 | test(`normalize ${fixtureName}`, async () => { 10 | const raw = JSON.parse( 11 | readFileSync(`./scenarios/${fixtureName}/raw-fixture.json`), 12 | ); 13 | const expected = JSON.parse( 14 | readFileSync(`./scenarios/${fixtureName}/normalized-fixture.json`), 15 | ); 16 | 17 | const scenarioState = { 18 | commitSha: {}, 19 | ids: {}, 20 | }; 21 | const actual = []; 22 | for (let item of raw.filter(isntIgnored)) { 23 | let result = await normalize.bind(null, scenarioState)(item); 24 | actual.push(result); 25 | } 26 | expect(actual).toEqual(expected); 27 | }); 28 | }); 29 | 30 | function isntIgnored(fixture) { 31 | return !fixture.reqheaders["x-octokit-fixture-ignore"]; 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) GitHub 2025 - Licensed as MIT. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/normalize/issue.js: -------------------------------------------------------------------------------- 1 | export default normalizeIssue; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import normalizeUser from "./user.js"; 6 | import setIfExists from "../set-if-exists.js"; 7 | 8 | function normalizeIssue(scenarioState, response) { 9 | // set all IDs to 1 10 | setIfExists( 11 | response, 12 | "id", 13 | fixturizeEntityId.bind(null, scenarioState.ids, "issue"), 14 | ); 15 | 16 | // set all dates to Universe 2017 Keynote time 17 | setIfExists(response, "created_at", "2017-10-10T16:00:00Z"); 18 | setIfExists(response, "updated_at", "2017-10-10T16:00:00Z"); 19 | 20 | // set all counts to 42 21 | setIfExists(response, "comments", 42); 22 | 23 | // normalize temporary repository 24 | [ 25 | "url", 26 | "repository_url", 27 | "labels_url", 28 | "comments_url", 29 | "events_url", 30 | "html_url", 31 | "timeline_url", 32 | ].forEach((property) => { 33 | setIfExists(response, property, fixturizePath.bind(null, scenarioState)); 34 | }); 35 | 36 | normalizeUser(scenarioState, response.user); 37 | } 38 | -------------------------------------------------------------------------------- /lib/normalize/combined-status.js: -------------------------------------------------------------------------------- 1 | export default normalizeCombinedStatus; 2 | 3 | import fixturizeCommitSha from "../fixturize-commit-sha.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import normalizeRepository from "./repository.js"; 6 | import normalizeStatus from "./status.js"; 7 | import setIfExists from "../set-if-exists.js"; 8 | 9 | function normalizeCombinedStatus(scenarioState, response) { 10 | const sha = response.sha; 11 | 12 | // set all dates to Universe 2017 Keynote time 13 | setIfExists(response, "created_at", "2017-10-10T16:00:00Z"); 14 | setIfExists(response, "updated_at", "2017-10-10T16:00:00Z"); 15 | 16 | // fixturize sha 17 | setIfExists( 18 | response, 19 | "sha", 20 | fixturizeCommitSha(scenarioState.commitSha, sha), 21 | ); 22 | 23 | // normalize temporary repository & fixturize sha in URLs 24 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 25 | setIfExists(response, "commit_url", fixturizePath.bind(null, scenarioState)); 26 | 27 | response.statuses.forEach(normalizeStatus.bind(null, scenarioState)); 28 | normalizeRepository(scenarioState, response.repository); 29 | } 30 | -------------------------------------------------------------------------------- /lib/normalize/commit.js: -------------------------------------------------------------------------------- 1 | export default normalizeCommit; 2 | 3 | import get from "lodash/get.js"; 4 | 5 | import fixturizeCommitSha from "../fixturize-commit-sha.js"; 6 | import fixturizePath from "../fixturize-path.js"; 7 | import setIfExists from "../set-if-exists.js"; 8 | 9 | function normalizeCommit(scenarioState, response) { 10 | const sha = response.sha; 11 | const treeSha = get(response, "tree.sha"); 12 | const fixturizedTreeSha = fixturizeCommitSha( 13 | scenarioState.commitSha, 14 | treeSha, 15 | ); 16 | const fixturizedSha = fixturizeCommitSha(scenarioState.commitSha, sha); 17 | 18 | // fixturize commit sha hashes 19 | setIfExists(response, "tree.sha", fixturizedTreeSha); 20 | setIfExists(response, "sha", fixturizedSha); 21 | 22 | // set all dates to Universe 2017 Keynote time 23 | setIfExists(response, "author.date", "2017-10-10T16:00:00Z"); 24 | setIfExists(response, "committer.date", "2017-10-10T16:00:00Z"); 25 | 26 | // normalize temporary repository 27 | setIfExists(response, "url", fixturizePath.bind(null, scenarioState)); 28 | setIfExists(response, "html_url", fixturizePath.bind(null, scenarioState)); 29 | setIfExists(response, "tree.url", fixturizePath.bind(null, scenarioState)); 30 | } 31 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-content/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Get repository", async () => { 6 | const mock = fixtures.mock("api.github.com/get-content"); 7 | 8 | const jsonResult = await axios({ 9 | method: "get", 10 | url: "https://api.github.com/repos/octokit-fixture-org/hello-world/contents/", 11 | headers: { 12 | Accept: "application/vnd.github.v3+json", 13 | Authorization: "token 0000000000000000000000000000000000000001", 14 | }, 15 | }).catch(mock.explain); 16 | 17 | expect(jsonResult.data.length).toBe(1); 18 | expect(jsonResult.data[0].path).toBe("README.md"); 19 | 20 | const rawResult = await axios({ 21 | method: "get", 22 | url: "https://api.github.com/repos/octokit-fixture-org/hello-world/contents/README.md", 23 | headers: { 24 | Accept: "application/vnd.github.v3.raw", 25 | Authorization: "token 0000000000000000000000000000000000000001", 26 | }, 27 | }).catch(mock.explain); 28 | 29 | expect(rawResult.data).toBe("# hello-world"); 30 | expect(rawResult.headers["content-type"]).toBe( 31 | "application/vnd.github.v3.raw; charset=utf-8", 32 | ); 33 | 34 | expect(mock.done.bind(mock)).not.toThrow(); 35 | }); 36 | -------------------------------------------------------------------------------- /scenarios/api.github.com/lock-issue/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Lock issues", async () => { 6 | const mock = fixtures.mock("api.github.com/lock-issue"); 7 | 8 | // https://developer.github.com/v3/issues/#lock-an-issue 9 | await axios({ 10 | method: "put", 11 | url: "https://api.github.com/repos/octokit-fixture-org/lock-issue/issues/1/lock", 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | Authorization: "token 0000000000000000000000000000000000000001", 15 | // > For PUT requests with no body attribute, be sure 16 | // to set the Content-Length header to zero. 17 | // https://developer.github.com/v3/#http-verbs 18 | "Content-Length": "0", 19 | }, 20 | }).catch(mock.explain); 21 | 22 | // https://developer.github.com/v3/issues/#unlock-an-issue 23 | await axios({ 24 | method: "delete", 25 | url: "https://api.github.com/repos/octokit-fixture-org/lock-issue/issues/1/lock", 26 | headers: { 27 | Accept: "application/vnd.github.v3+json", 28 | Authorization: "token 0000000000000000000000000000000000000001", 29 | }, 30 | }).catch(mock.explain); 31 | 32 | expect(mock.done.bind(mock)).not.toThrow(); 33 | }); 34 | -------------------------------------------------------------------------------- /scenarios/api.github.com/paginate-issues/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("paginate issues", async () => { 6 | const mock = fixtures.mock("api.github.com/paginate-issues"); 7 | 8 | // https://developer.github.com/v3/issues/#list-issues-for-a-repository 9 | // Get pages 1 - 5. 10 | const options = { 11 | method: "get", 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | Authorization: "token 0000000000000000000000000000000000000001", 15 | }, 16 | }; 17 | 18 | const urls = [ 19 | "https://api.github.com/repos/octokit-fixture-org/paginate-issues/issues?per_page=3", 20 | "https://api.github.com/repositories/1000/issues?per_page=3&page=2", 21 | "https://api.github.com/repositories/1000/issues?per_page=3&page=3", 22 | "https://api.github.com/repositories/1000/issues?per_page=3&page=4", 23 | "https://api.github.com/repositories/1000/issues?per_page=3&page=5", 24 | ]; 25 | 26 | for (let i = 0; i < urls.length; i++) { 27 | await axios 28 | .request( 29 | Object.assign(options, { 30 | url: urls[i], 31 | }), 32 | ) 33 | .catch(mock.explain); 34 | } 35 | 36 | expect(mock.done.bind(mock)).not.toThrow(); 37 | }); 38 | -------------------------------------------------------------------------------- /scenarios/api.github.com/create-file/record.js: -------------------------------------------------------------------------------- 1 | export default createFile; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function createFile(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "create-file", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/repos/contents/#create-a-file 19 | await state.request({ 20 | method: "put", 21 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/contents/test.txt`, 22 | headers: { 23 | Accept: "application/vnd.github.v3+json", 24 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 25 | }, 26 | data: { 27 | message: "create test.txt", 28 | content: Buffer.from("Test content").toString("base64"), 29 | }, 30 | }); 31 | } catch (_error) { 32 | error = _error; 33 | } 34 | 35 | await temporaryRepository.delete(); 36 | 37 | if (error) { 38 | return Promise.reject(error); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/normalize/release.js: -------------------------------------------------------------------------------- 1 | export default normalizeReleaseAsset; 2 | import fixturizeEntityId from "../fixturize-entity-id.js"; 3 | import fixturizeCommitSha from "../fixturize-commit-sha.js"; 4 | import fixturizePath from "../fixturize-path.js"; 5 | import normalizeUser from "./user.js"; 6 | import setIfExists from "../set-if-exists.js"; 7 | 8 | function normalizeReleaseAsset(scenarioState, response) { 9 | // set ID to 1 10 | setIfExists( 11 | response, 12 | "id", 13 | fixturizeEntityId.bind(null, scenarioState.ids, "release"), 14 | ); 15 | 16 | // fixturize commit sha hashes 17 | setIfExists( 18 | response, 19 | "target_commitish", 20 | fixturizeCommitSha(scenarioState.commitSha, response.target_commitish), 21 | ); 22 | 23 | // set all dates to Universe 2017 Keynote time 24 | setIfExists(response, "created_at", "2017-10-10T16:00:00Z"); 25 | setIfExists(response, "published_at", "2017-10-10T16:00:00Z"); 26 | 27 | // normalize temporary repository name & id in URLs 28 | [ 29 | "assets_url", 30 | "html_url", 31 | "tarball_url", 32 | "upload_url", 33 | "url", 34 | "zipball_url", 35 | ].forEach((property) => { 36 | setIfExists(response, property, fixturizePath.bind(null, scenarioState)); 37 | }); 38 | 39 | normalizeUser(scenarioState, response.author); 40 | } 41 | -------------------------------------------------------------------------------- /scenarios/api.github.com/add-labels-to-issue/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Get repository", async () => { 6 | const mock = fixtures.mock("api.github.com/add-labels-to-issue"); 7 | 8 | // https://developer.github.com/v3/issues/#create-an-issue 9 | await axios({ 10 | method: "post", 11 | url: "https://api.github.com/repos/octokit-fixture-org/add-labels-to-issue/issues", 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | Authorization: "token 0000000000000000000000000000000000000001", 15 | "Content-Type": "application/json; charset=utf-8", 16 | }, 17 | data: { 18 | title: "Issue without a label", 19 | }, 20 | }); 21 | 22 | // https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue 23 | await axios({ 24 | method: "post", 25 | url: "https://api.github.com/repos/octokit-fixture-org/add-labels-to-issue/issues/1/labels", 26 | headers: { 27 | Accept: "application/vnd.github.v3+json", 28 | Authorization: "token 0000000000000000000000000000000000000001", 29 | "Content-Type": "application/json; charset=utf-8", 30 | }, 31 | data: { 32 | labels: ["Foo", "bAr", "baZ"], 33 | }, 34 | }); // .catch(mock.explain) 35 | 36 | expect(mock.done.bind(mock)).not.toThrow(); 37 | }); 38 | -------------------------------------------------------------------------------- /scenarios/api.github.com/errors/record.js: -------------------------------------------------------------------------------- 1 | export default errors; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function errors(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "errors", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/issues/labels/#create-a-label 19 | // Create a label with invalid payload 20 | await state 21 | .request({ 22 | method: "post", 23 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/labels`, 24 | headers: { 25 | Accept: "application/vnd.github.v3+json", 26 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 27 | }, 28 | data: { 29 | name: "foo", 30 | color: "invalid", 31 | }, 32 | }) 33 | 34 | // record expected 422 error 35 | .catch((error) => { 36 | if (error.response.status !== 422) { 37 | throw error; 38 | } 39 | }); 40 | } catch (_error) { 41 | error = _error; 42 | } 43 | 44 | await temporaryRepository.delete(); 45 | 46 | if (error) { 47 | return Promise.reject(error); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/immediate-response.yml: -------------------------------------------------------------------------------- 1 | name: Issue/PR response 2 | permissions: 3 | issues: write 4 | pull-requests: write 5 | on: 6 | issues: 7 | types: 8 | - opened 9 | pull_request_target: 10 | types: 11 | - opened 12 | jobs: 13 | respond-to-issue: 14 | if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]' && github.actor != 'githubactions[bot]' && github.actor != 'octokitbot' }} 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Determine issue or PR number 18 | id: extract 19 | run: echo "NUMBER=${{ github.event.issue.number || github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" 20 | 21 | - name: Respond to issue or PR 22 | uses: peter-evans/create-or-update-comment@v5 23 | with: 24 | issue-number: ${{ steps.extract.outputs.NUMBER }} 25 | body: > 26 | 👋 Hi! Thank you for this contribution! Just to let you know, our GitHub SDK team does a round of issue and PR reviews twice a week, every Monday and Friday! 27 | We have a [process in place](https://github.com/octokit/.github/blob/main/community/prioritization_response.md#overview) for prioritizing and responding to your input. 28 | Because you are a part of this community please feel free to comment, add to, or pick up any issues/PRs that are labeled with `Status: Up for grabs`. 29 | You & others like you are the reason all of this works! So thank you & happy coding! 🚀 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: File a bug report 3 | title: "[BUG]: " 4 | labels: ["Type: Bug", "Status: Triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: What did you do? What happened? What did you expect to happen? 15 | placeholder: Put your description of the bug here. 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: versions 20 | attributes: 21 | label: Versions 22 | description: What versions of the relevant software are you running? 23 | placeholder: Octokit.js v2.0.10, Node v16.18.0 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: logs 28 | attributes: 29 | label: Relevant log output 30 | description: | 31 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 32 | Please check your logs before submission to ensure sensitive information is redacted. 33 | render: shell 34 | - type: checkboxes 35 | id: terms 36 | attributes: 37 | label: Code of Conduct 38 | description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) 39 | options: 40 | - label: I agree to follow this project's Code of Conduct 41 | required: true 42 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-archive/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Get archive", async () => { 6 | const mock = fixtures.mock("api.github.com/get-archive"); 7 | 8 | // https://developer.github.com/v3/repos/#edit 9 | const redirectLocation = await axios({ 10 | method: "get", 11 | url: "https://api.github.com/repos/octokit-fixture-org/get-archive/tarball/main", 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | Authorization: "token 0000000000000000000000000000000000000001", 15 | }, 16 | // axios (or the lower level follow-redirects package) does not handle 307 17 | // redirects correctly 18 | maxRedirects: 0, 19 | }) 20 | .catch((error) => { 21 | expect(error.response.status).toBe(302); 22 | if (error.response.status === 302) { 23 | return error.response.headers.location; 24 | } 25 | 26 | throw error; 27 | }) 28 | .catch(mock.explain); 29 | 30 | expect(redirectLocation).toBe( 31 | "https://codeload.github.com/octokit-fixture-org/get-archive/legacy.tar.gz/refs/heads/main", 32 | ); 33 | 34 | const result = await axios({ 35 | method: "get", 36 | url: redirectLocation, 37 | headers: { 38 | Accept: "application/vnd.github.v3+json", 39 | Authorization: "token 0000000000000000000000000000000000000001", 40 | }, 41 | }); 42 | 43 | expect(Buffer.from(result.data, "binary").toString("hex").length).toBe(332); 44 | 45 | expect(mock.done.bind(mock)).not.toThrow(); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/record-scenario.js: -------------------------------------------------------------------------------- 1 | export default recordScenario; 2 | 3 | import nock from "nock"; 4 | const { recorder, restore } = nock; 5 | import removeCredentials from "./remove-credentials.js"; 6 | 7 | async function recordScenario({ request, scenario }) { 8 | recorder.rec({ 9 | output_objects: true, 10 | dont_print: true, 11 | enable_reqheaders_recording: true, 12 | }); 13 | 14 | if (Array.isArray(scenario)) { 15 | // if scenario is an array of request options, send requests sequentially 16 | await scenario.reduce(async (promise, step) => { 17 | let response; 18 | 19 | try { 20 | response = await promise; 21 | } catch (error) { 22 | // don’t fail on 4xx errors, they are valid fixtures 23 | if (error.response.status >= 500) { 24 | throw error; 25 | } 26 | 27 | response = error.response; 28 | } 29 | 30 | if (typeof step === "function") { 31 | return request(step(response)); 32 | } 33 | 34 | return request(step); 35 | }, Promise.resolve()); 36 | } else if (typeof scenario === "object") { 37 | // if scenario is an object with request options, send a request for it 38 | await request(scenario); 39 | } else { 40 | // otherwise we expect scenario to be an asynchronous function 41 | await scenario({ request }); 42 | } 43 | 44 | const fixtures = recorder.play(); 45 | 46 | recorder.clear(); 47 | restore(); 48 | 49 | return fixtures.map(removeCredentials).map((fixture) => { 50 | fixture.method = fixture.method.toLowerCase(); 51 | return fixture; 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | "on": 2 | schedule: 3 | - cron: 5 4 * * * 4 | workflow_dispatch: {} 5 | name: Update 6 | jobs: 7 | update: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v6 11 | - uses: actions/setup-node@v6 12 | with: 13 | node-version: 24 14 | cache: npm 15 | - run: git checkout fixtures-update || true 16 | - run: npm ci 17 | - run: node bin/record --update 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | FIXTURES_USER_A_TOKEN_FULL_ACCESS: ${{ secrets.FIXTURES_USER_A_TOKEN_FULL_ACCESS }} 21 | FIXTURES_USER_B_TOKEN_FULL_ACCESS: ${{ secrets.FIXTURES_USER_B_TOKEN_FULL_ACCESS }} 22 | - name: create pull request 23 | uses: gr2m/create-or-update-pull-request-action@v1.x 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.OCTOKITBOT_PAT }} 26 | with: 27 | title: 🚧 🤖📯 Fixtures changed 28 | body: > 29 | I found new changes in the recorded fixtures 👋🤖 30 | 31 | 32 | I can't tell if the changes are fixes, features or breaking, you'll 33 | have to figure that out on yourself and adapt the commit messages 34 | accordingly to trigger the right release, see [our commit message 35 | conventions](https://github.com/octokit/openapi/blob/main/CONTRIBUTING.md#merging-the-pull-request--releasing-a-new-version). 36 | branch: fixtures-update 37 | author: Octokit Bot 38 | commit-message: "WIP: fixtures changed - please review" 39 | labels: "Type: Maintenance" 40 | -------------------------------------------------------------------------------- /scenarios/api.github.com/add-labels-to-issue/record.js: -------------------------------------------------------------------------------- 1 | export default addAndRemoveRepositoryCollaborator; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | // - create issue 6 | // - add labels to issue 7 | async function addAndRemoveRepositoryCollaborator(state) { 8 | let error; 9 | // create a temporary repository 10 | const temporaryRepository = getTemporaryRepository({ 11 | request: state.request, 12 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 13 | org: "octokit-fixture-org", 14 | name: "add-labels-to-issue", 15 | }); 16 | 17 | await temporaryRepository.create(); 18 | 19 | try { 20 | // https://developer.github.com/v3/issues/#create-an-issue 21 | await state.request({ 22 | method: "post", 23 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/issues`, 24 | headers: { 25 | Accept: "application/vnd.github.v3+json", 26 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 27 | }, 28 | data: { 29 | title: "Issue without a label", 30 | }, 31 | }); 32 | 33 | // https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue 34 | await state.request({ 35 | method: "post", 36 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/issues/1/labels`, 37 | headers: { 38 | Accept: "application/vnd.github.v3+json", 39 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 40 | }, 41 | data: { 42 | labels: ["Foo", "bAr", "baZ"], 43 | }, 44 | }); 45 | } catch (_error) { 46 | error = _error; 47 | } 48 | 49 | await temporaryRepository.delete(); 50 | 51 | if (error) { 52 | return Promise.reject(error); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scenarios/api.github.com/markdown/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Get repository", async () => { 6 | const mock = fixtures.mock("api.github.com/markdown"); 7 | 8 | const { data: contextMarkdown } = await axios({ 9 | method: "post", 10 | url: "https://api.github.com/markdown", 11 | headers: { 12 | Accept: "text/html", 13 | Authorization: "token 0000000000000000000000000000000000000001", 14 | "Content-Type": "application/json; charset=utf-8", 15 | }, 16 | data: { 17 | text: `### Hello 18 | 19 | b597b5d`, 20 | context: "octokit-fixture-org/hello-world", 21 | mode: "gfm", 22 | }, 23 | }).catch(mock.explain); 24 | 25 | expect(contextMarkdown).toBe( 26 | '

Hello

\n

b597b5d

', 27 | ); 28 | 29 | const { data: markdown } = await axios({ 30 | method: "post", 31 | url: "https://api.github.com/markdown/raw", 32 | headers: { 33 | Accept: "text/html", 34 | Authorization: "token 0000000000000000000000000000000000000001", 35 | "Content-Type": "text/plain; charset=utf-8", 36 | }, 37 | data: `### Hello 38 | 39 | b597b5d`, 40 | }).catch(mock.explain); 41 | 42 | expect(markdown).toBe( 43 | '

\nHello

\n

b597b5d

\n', 44 | ); 45 | expect(mock.done.bind(mock)).not.toThrow(); 46 | }); 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Update or add documentation 3 | title: "[DOCS]: " 4 | labels: ["Type: Documentation", "Status: Triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this out! 10 | - type: textarea 11 | id: describe-need 12 | attributes: 13 | label: Describe the need 14 | description: What do you wish was different about our docs? 15 | placeholder: Describe the need for documentation updates here. 16 | validations: 17 | required: true 18 | - type: input 19 | id: sdk_version 20 | attributes: 21 | label: SDK Version 22 | description: Do these docs apply to a specific SDK version? 23 | placeholder: Octokit.NET v4.0.1 24 | validations: 25 | required: false 26 | - type: input 27 | id: api_version 28 | attributes: 29 | label: API Version 30 | description: Do these docs apply to a specific version of the GitHub REST API or GraphQL API? 31 | placeholder: ex. v1.1.1 32 | validations: 33 | required: false 34 | - type: textarea 35 | id: logs 36 | attributes: 37 | label: Relevant log output 38 | description: | 39 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 40 | Please check your logs before submission to ensure sensitive information is redacted. 41 | render: shell 42 | - type: checkboxes 43 | id: terms 44 | attributes: 45 | label: Code of Conduct 46 | description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) 47 | options: 48 | - label: I agree to follow this project's Code of Conduct 49 | required: true 50 | -------------------------------------------------------------------------------- /lib/temporary-repository.js: -------------------------------------------------------------------------------- 1 | export default temporaryRepository; 2 | 3 | export const regex = /tmp-scenario-([^/]+)-\d{17}-\w{5}/; 4 | 5 | function temporaryRepository({ org, name, request, token }) { 6 | const state = { org, name, request, token }; 7 | const nowTimestamp = new Date().toISOString().replace(/\D/g, ""); 8 | const random = Math.random().toString(36).substr(2, 5); 9 | state.temporaryName = `tmp-scenario-${name}-${nowTimestamp}-${random}`; 10 | 11 | return { 12 | name: state.temporaryName, 13 | create: createTemporaryRepository.bind(null, state), 14 | delete: deleteTemporaryRepository.bind(null, state), 15 | }; 16 | } 17 | 18 | async function createTemporaryRepository(state) { 19 | // https://developer.github.com/v3/repos/#create 20 | const response = state.request({ 21 | method: "post", 22 | url: `/orgs/${state.org}/repos`, 23 | headers: { 24 | Accept: "application/vnd.github.v3+json", 25 | Authorization: `token ${state.token}`, 26 | "X-Octokit-Fixture-Ignore": "true", 27 | }, 28 | data: { 29 | name: state.temporaryName, 30 | }, 31 | }); 32 | 33 | await new Promise((resolve) => setTimeout(resolve, 1000)); 34 | 35 | /* istanbul ignore next - https://github.com/octokit/fixtures/pull/425#issuecomment-863613769 */ 36 | return response; 37 | } 38 | 39 | function deleteTemporaryRepository(state) { 40 | // https://developer.github.com/v3/repos/#create 41 | return state.request({ 42 | method: "delete", 43 | url: `/repos/${state.org}/${state.temporaryName}`, 44 | headers: { 45 | Accept: "application/vnd.github.v3+json", 46 | Authorization: `token ${state.token}`, 47 | "X-Octokit-Fixture-Ignore": "true", 48 | }, 49 | // delete repositories that have been renamed in the scenario 50 | maxRedirects: 1, 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/maintenance.yml: -------------------------------------------------------------------------------- 1 | name: Maintenance 2 | description: Dependencies, cleanup, refactoring, reworking of code 3 | title: "[MAINT]: " 4 | labels: ["Type: Maintenance", "Status: Triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this out! 10 | - type: textarea 11 | id: describe-need 12 | attributes: 13 | label: Describe the need 14 | description: What do you want to happen? 15 | placeholder: Describe the maintenance need here. 16 | validations: 17 | required: true 18 | - type: input 19 | id: sdk_version 20 | attributes: 21 | label: SDK Version 22 | description: Does this maintenance apply to a specific SDK version? 23 | placeholder: terraform-provider-github v5.7.0 24 | validations: 25 | required: false 26 | - type: input 27 | id: api_version 28 | attributes: 29 | label: API Version 30 | description: Does this maintenance apply to a specific version of the GitHub REST API or GraphQL API? 31 | placeholder: ex. v1.1.1 32 | validations: 33 | required: false 34 | - type: textarea 35 | id: logs 36 | attributes: 37 | label: Relevant log output 38 | description: | 39 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 40 | Please check your logs before submission to ensure sensitive information is redacted. 41 | render: shell 42 | - type: checkboxes 43 | id: terms 44 | attributes: 45 | label: Code of Conduct 46 | description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) 47 | options: 48 | - label: I agree to follow this project's Code of Conduct 49 | required: true 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature 2 | description: Suggest an idea for a new feature or enhancement 3 | title: "[FEAT]: " 4 | labels: ["Type: Feature", "Status: Triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this out! 10 | - type: textarea 11 | id: describe-need 12 | attributes: 13 | label: Describe the need 14 | description: What do you want to happen? What problem are you trying to solve? 15 | placeholder: Describe the need for the feature. 16 | validations: 17 | required: true 18 | - type: input 19 | id: sdk_version 20 | attributes: 21 | label: SDK Version 22 | description: Does this feature suggestion apply to a specific SDK version? 23 | placeholder: Octokit.rb v6.0.0 24 | validations: 25 | required: false 26 | - type: input 27 | id: api_version 28 | attributes: 29 | label: API Version 30 | description: Does this feature suggestion apply to a specific version of the GitHub REST API or GraphQL API? 31 | placeholder: ex. v1.1.1 32 | validations: 33 | required: false 34 | - type: textarea 35 | id: logs 36 | attributes: 37 | label: Relevant log output 38 | description: | 39 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 40 | Please check your logs before submission to ensure sensitive information is redacted. 41 | render: shell 42 | - type: checkboxes 43 | id: terms 44 | attributes: 45 | label: Code of Conduct 46 | description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) 47 | options: 48 | - label: I agree to follow this project's Code of Conduct 49 | required: true 50 | -------------------------------------------------------------------------------- /lib/to-entity-name.js: -------------------------------------------------------------------------------- 1 | export default toEntityName; 2 | 3 | function toEntityName(object, fixture) { 4 | // object is binary response, so we check for it above the object check 5 | if (/\/legacy\.(tar\.gz|zip)\/refs\/heads\/main$/.test(fixture.path)) { 6 | return "archive"; 7 | } 8 | 9 | if (typeof object !== "object") { 10 | return; 11 | } 12 | 13 | if (object.type === "Organization") { 14 | return "organization"; 15 | } 16 | if ("forks" in object) { 17 | return "repository"; 18 | } 19 | if (/\/repositories\/\d+$/.test(object.url)) { 20 | return "repository"; 21 | } 22 | if ("invitee" in object && "inviter" in object) { 23 | return "invitation"; 24 | } 25 | if ("number" in object && /\/issues\/\d+$/.test(object.url)) { 26 | return "issue"; 27 | } 28 | if ("color" in object && /\/labels\//.test(object.url)) { 29 | return "label"; 30 | } 31 | if ("content" in object && "commit" in object) { 32 | return "file-change"; 33 | } 34 | if (/\/statuses\/[0-9a-f]{40}$/.test(object.url)) { 35 | return "status"; 36 | } 37 | if (/\/commits\/[0-9a-f]{40}\/status$/.test(object.url)) { 38 | return "combined-status"; 39 | } 40 | if ("ref" in object) { 41 | return "reference"; 42 | } 43 | if (/\/protection$/.test(object.url)) { 44 | return "branch-protection"; 45 | } 46 | if ("prerelease" in object) { 47 | return "release"; 48 | } 49 | if (/\/releases\/assets\/\d+$/.test(object.url)) { 50 | return "release-asset"; 51 | } 52 | if (/\/projects\/columns\/cards\/\d+$/.test(object.url)) { 53 | return "project-card"; 54 | } 55 | if (/\/projects\/columns\/cards\/\d+\/moves$/.test(fixture.path)) { 56 | return "project-card-move"; 57 | } 58 | if (/^\/search\/issues\?/.test(fixture.path)) { 59 | return "search-issues"; 60 | } 61 | 62 | if ("errors" in object) { 63 | return "error"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /scenarios/api.github.com/mark-notifications-as-read/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "put", 5 | "path": "/notifications", 6 | "body": "", 7 | "status": 205, 8 | "response": "", 9 | "reqheaders": { 10 | "accept": "application/vnd.github.v3+json", 11 | "authorization": "token 0000000000000000000000000000000000000001", 12 | "host": "api.github.com", 13 | "content-length": 0 14 | }, 15 | "responseIsBinary": false, 16 | "headers": { 17 | "access-control-allow-origin": "*", 18 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 19 | "connection": "close", 20 | "content-length": "0", 21 | "content-security-policy": "default-src 'none'", 22 | "content-type": "text/plain;charset=utf-8", 23 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 24 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 25 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 26 | "x-accepted-oauth-scopes": "notifications, repo", 27 | "x-content-type-options": "nosniff", 28 | "x-frame-options": "deny", 29 | "x-github-media-type": "github.v3; format=json", 30 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 31 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 32 | "x-ratelimit-limit": "5000", 33 | "x-ratelimit-remaining": "4999", 34 | "x-ratelimit-reset": "1507651200000", 35 | "x-ratelimit-resource": "core", 36 | "x-ratelimit-used": 1, 37 | "x-xss-protection": "0" 38 | } 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /scenarios/api.github.com/lock-issue/record.js: -------------------------------------------------------------------------------- 1 | export default lockIssue; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function lockIssue(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "lock-issue", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/issues/#create-an-issue 19 | // (this request gets ignored, we need an existing issue that we can lock) 20 | await state.request({ 21 | method: "post", 22 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/issues`, 23 | headers: { 24 | Accept: "application/vnd.github.v3+json", 25 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 26 | "X-Octokit-Fixture-Ignore": "true", 27 | }, 28 | data: { 29 | title: "Issue without a label", 30 | }, 31 | }); 32 | 33 | // https://developer.github.com/v3/issues/#lock-an-issue 34 | await state.request({ 35 | method: "put", 36 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/issues/1/lock`, 37 | headers: { 38 | Accept: "application/vnd.github.v3+json", 39 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 40 | }, 41 | }); 42 | 43 | // https://developer.github.com/v3/issues/#unlock-an-issue 44 | await state.request({ 45 | method: "delete", 46 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/issues/1/lock`, 47 | headers: { 48 | Accept: "application/vnd.github.v3+json", 49 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 50 | }, 51 | }); 52 | } catch (_error) { 53 | error = _error; 54 | } 55 | 56 | await temporaryRepository.delete(); 57 | 58 | if (error) { 59 | return Promise.reject(error); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/unit/temporary-repository.test.js: -------------------------------------------------------------------------------- 1 | import temporaryRepository, { regex } from "../../lib/temporary-repository.js"; 2 | 3 | test("temporaryRepository(name) returns {create, delete} API", () => { 4 | const options = {}; 5 | const api = temporaryRepository(options); 6 | 7 | expect(api.create).toBeInstanceOf(Function); 8 | expect(api.delete).toBeInstanceOf(Function); 9 | }); 10 | 11 | test("temporaryRepository(name) returns {name}", () => { 12 | const options = { 13 | name: "foo-bar", 14 | }; 15 | const api = temporaryRepository(options); 16 | 17 | expect(api.name).toMatch(regex); 18 | // `"${api.name}" matches tmp repository name regex` 19 | }); 20 | 21 | test("temporaryRepository.regex", () => { 22 | const { name } = temporaryRepository({ name: "funky-repo" }); 23 | const [, originalName] = name.match(regex); 24 | expect(originalName).toBe("funky-repo"); 25 | expect(`/repos/org-foo/${name}`.replace(regex, "$1")).toBe( 26 | "/repos/org-foo/funky-repo", 27 | ); 28 | }); 29 | 30 | test("temporaryRepository(name).create() sends POST /orgs/octokit-fixture-org/repos request", () => { 31 | expect.assertions(4); 32 | 33 | const options = { 34 | name: "repo-bar", 35 | token: "token123", 36 | org: "org-foo", 37 | request(options) { 38 | expect(options.method).toBe("post"); 39 | expect(options.url).toBe("/orgs/org-foo/repos"); 40 | expect(options.headers.Authorization).toBe("token token123"); 41 | expect(options.data.name).toBe(api.name); 42 | return Promise.resolve(); 43 | }, 44 | }; 45 | const api = temporaryRepository(options); 46 | api.create(); 47 | }); 48 | 49 | test("temporaryRepository(name).delete() sends DELETE `/repos/octokit-fixture-org/ request", () => { 50 | expect.assertions(3); 51 | 52 | const options = { 53 | name: "repo-bar", 54 | token: "token123", 55 | org: "org-foo", 56 | request(options) { 57 | expect(options.method).toBe("delete"); 58 | expect(options.url).toBe(`/repos/org-foo/${api.name}`); 59 | expect(options.headers.Authorization).toBe("token token123"); 60 | }, 61 | }; 62 | const api = temporaryRepository(options); 63 | api.delete(); 64 | }); 65 | -------------------------------------------------------------------------------- /scenarios/api.github.com/errors/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "post", 5 | "path": "/repos/octokit-fixture-org/errors/labels", 6 | "body": { "name": "foo", "color": "invalid" }, 7 | "status": 422, 8 | "response": { 9 | "message": "Validation Failed", 10 | "errors": [{ "resource": "Label", "code": "invalid", "field": "color" }], 11 | "documentation_url": "https://docs.github.com/rest/reference/issues#create-a-label" 12 | }, 13 | "reqheaders": { 14 | "accept": "application/vnd.github.v3+json", 15 | "content-type": "application/json; charset=utf-8", 16 | "authorization": "token 0000000000000000000000000000000000000001", 17 | "content-length": 32, 18 | "host": "api.github.com" 19 | }, 20 | "responseIsBinary": false, 21 | "headers": { 22 | "access-control-allow-origin": "*", 23 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 24 | "connection": "close", 25 | "content-length": "179", 26 | "content-security-policy": "default-src 'none'", 27 | "content-type": "application/json; charset=utf-8", 28 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 29 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 30 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 31 | "x-accepted-oauth-scopes": "", 32 | "x-content-type-options": "nosniff", 33 | "x-frame-options": "deny", 34 | "x-github-media-type": "github.v3; format=json", 35 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 36 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 37 | "x-ratelimit-limit": "5000", 38 | "x-ratelimit-remaining": "4999", 39 | "x-ratelimit-reset": "1507651200000", 40 | "x-ratelimit-resource": "core", 41 | "x-ratelimit-used": 1, 42 | "x-xss-protection": "0" 43 | } 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /lib/normalize/archive.js: -------------------------------------------------------------------------------- 1 | export default normalizeArchive; 2 | 3 | import { createGzip } from "zlib"; 4 | 5 | import intoStream from "into-stream"; 6 | import { getStreamAsBuffer } from "get-stream"; 7 | import { extract as _extract, pack as _pack } from "tar-stream"; 8 | import gunzip from "gunzip-maybe"; 9 | 10 | import { regex } from "../temporary-repository.js"; 11 | 12 | async function normalizeArchive(scenarioState, response, fixture) { 13 | fixture.headers["content-disposition"] = fixture.headers[ 14 | "content-disposition" 15 | ] 16 | // normalize folder name in file name 17 | .replace(regex, "$1") 18 | // zero sha 19 | .replace(/archive-\w{7}/, "archive-0000000"); 20 | 21 | const extract = _extract(); 22 | const pack = _pack(); 23 | const readStream = intoStream(Buffer.from(response, "hex")); 24 | 25 | // The response is the Repository folder with the README.md file inside. The 26 | // folder name is always different, based on the repository name when recorded. 27 | // That's why we have to untar/zip the response, change the folder name and 28 | // retar/zip it again. 29 | 30 | extract.on("entry", function (header, stream, callback) { 31 | header.name = header.name 32 | // normalize folder name in path 33 | .replace(regex, "$1") 34 | // zero sha in path 35 | .replace(/-(\w){7}\//, "-0000000/"); 36 | 37 | // normalize mtime 38 | header.mtime = { 39 | getTime: () => 1507651200000, 40 | }; 41 | 42 | // write the new entry to the pack stream 43 | stream.pipe(pack.entry(header, callback)); 44 | }); 45 | 46 | extract.on("finish", function () { 47 | // all entries done - lets finalize it 48 | pack.finalize(); 49 | }); 50 | 51 | // pipe the old tarball to the extractor 52 | readStream.pipe(gunzip()).pipe(extract); 53 | 54 | // pipe the new tarball the another stream 55 | const writeStream = pack.pipe(createGzip()); 56 | 57 | const result = await getStreamAsBuffer(writeStream).catch(console.log); 58 | fixture.response = result.toString("hex"); 59 | 60 | // normalize across operating systems / extra flags 61 | // see http://www.zlib.org/rfc-gzip.html#header-trailer 62 | const normalizedHeader = "1f8b0800000000000003"; 63 | fixture.response = 64 | normalizedHeader + fixture.response.substr(normalizedHeader.length); 65 | } 66 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-archive/record.js: -------------------------------------------------------------------------------- 1 | export default getArchive; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function getArchive(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "get-archive", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/repos/contents/#create-a-file 19 | // (this request gets ignored, we need an existing content to download it) 20 | await state.request({ 21 | method: "put", 22 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/contents/README.md`, 23 | headers: { 24 | Accept: "application/vnd.github.v3+json", 25 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 26 | "X-Octokit-Fixture-Ignore": "true", 27 | }, 28 | data: { 29 | message: "initial commit", 30 | content: Buffer.from("# get-archive").toString("base64"), 31 | }, 32 | }); 33 | 34 | // https://developer.github.com/v3/repos/contents/#get-archive-link 35 | // Download repository as archive. state.request throws error for a 36 | // 3xx response so we have to catch that 37 | try { 38 | await state.request({ 39 | method: "get", 40 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/tarball/main`, 41 | headers: { 42 | Accept: "application/vnd.github.v3+json", 43 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 44 | }, 45 | }); 46 | } catch (error) { 47 | const { headers } = error.response; 48 | 49 | await state.request({ 50 | method: "get", 51 | url: headers.location, 52 | headers: { 53 | Accept: "application/vnd.github.v3+json", 54 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 55 | }, 56 | }); 57 | } 58 | } catch (_error) { 59 | error = _error; 60 | } 61 | 62 | await temporaryRepository.delete(); 63 | 64 | if (error) { 65 | return Promise.reject(error); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scenarios/api.github.com/paginate-issues/record.js: -------------------------------------------------------------------------------- 1 | export default paginateIssues; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function paginateIssues(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "paginate-issues", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // create 13 issues for our testing. We want 5 pages so that we can request 19 | // ?per_page=3&page=3 which has different URLs for first, previous, next and last page. 20 | // Why 13? ¯\_(ツ)_/¯ 21 | 22 | for (var i = 1; i <= 13; i++) { 23 | // https://developer.github.com/v3/issues/#create-an-issue 24 | // This request will be ignored 25 | await state.request({ 26 | method: "post", 27 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/issues`, 28 | headers: { 29 | Accept: "application/vnd.github.v3+json", 30 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 31 | "X-Octokit-Fixture-Ignore": "true", 32 | }, 33 | data: { 34 | title: `Test issue ${i}`, 35 | }, 36 | }); 37 | } 38 | 39 | // https://developer.github.com/v3/issues/#list-issues-for-a-repository 40 | // Get 1st page and then page 2-5 with page query parameter 41 | let url = `/repos/octokit-fixture-org/${temporaryRepository.name}/issues?per_page=3`; 42 | const options = { 43 | method: "get", 44 | headers: { 45 | Accept: "application/vnd.github.v3+json", 46 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 47 | }, 48 | }; 49 | 50 | for (let i = 1; i <= 5; i++) { 51 | const response = await state.request( 52 | Object.assign(options, { 53 | url, 54 | }), 55 | ); 56 | 57 | url = ((response.headers.link || "").match(/<([^>]+)>;\s*rel="next"/) || 58 | [])[1]; 59 | } 60 | } catch (_error) { 61 | error = _error; 62 | } 63 | 64 | await temporaryRepository.delete(); 65 | 66 | if (error) { 67 | return Promise.reject(error); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scenarios/api.github.com/mark-notifications-as-read/raw-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "put", 5 | "path": "/notifications", 6 | "body": "", 7 | "status": 205, 8 | "response": "", 9 | "rawHeaders": [ 10 | "Server", 11 | "GitHub.com", 12 | "Date", 13 | "Tue, 19 Jul 2022 04:38:33 GMT", 14 | "Content-Type", 15 | "text/plain;charset=utf-8", 16 | "Content-Length", 17 | "0", 18 | "X-OAuth-Scopes", 19 | "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 20 | "X-Accepted-OAuth-Scopes", 21 | "notifications, repo", 22 | "X-GitHub-Media-Type", 23 | "github.v3; format=json", 24 | "X-RateLimit-Limit", 25 | "5000", 26 | "X-RateLimit-Remaining", 27 | "4939", 28 | "X-RateLimit-Reset", 29 | "1658208999", 30 | "X-RateLimit-Used", 31 | "61", 32 | "X-RateLimit-Resource", 33 | "core", 34 | "Access-Control-Expose-Headers", 35 | "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 36 | "Access-Control-Allow-Origin", 37 | "*", 38 | "Strict-Transport-Security", 39 | "max-age=31536000; includeSubdomains; preload", 40 | "X-Frame-Options", 41 | "deny", 42 | "X-Content-Type-Options", 43 | "nosniff", 44 | "X-XSS-Protection", 45 | "0", 46 | "Referrer-Policy", 47 | "origin-when-cross-origin, strict-origin-when-cross-origin", 48 | "Content-Security-Policy", 49 | "default-src 'none'", 50 | "Vary", 51 | "Accept-Encoding, Accept, X-Requested-With", 52 | "X-GitHub-Request-Id", 53 | "0683:13C9:1369863:3E1A0DD:62D63549", 54 | "connection", 55 | "close" 56 | ], 57 | "reqheaders": { 58 | "accept": "application/vnd.github.v3+json", 59 | "content-type": "application/x-www-form-urlencoded", 60 | "authorization": "token 0000000000000000000000000000000000000001", 61 | "host": "api.github.com" 62 | }, 63 | "responseIsBinary": false 64 | } 65 | ] 66 | -------------------------------------------------------------------------------- /scenarios/api.github.com/create-status/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Create status", async () => { 6 | const mock = fixtures.mock("api.github.com/create-status"); 7 | 8 | // create failure status 9 | await axios({ 10 | method: "post", 11 | url: "https://api.github.com/repos/octokit-fixture-org/create-status/statuses/0000000000000000000000000000000000000001", 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | Authorization: "token 0000000000000000000000000000000000000001", 15 | "Content-Type": "application/json; charset=utf-8", 16 | }, 17 | data: { 18 | state: "failure", 19 | target_url: "https://example.com", 20 | description: "create-status failure test", 21 | context: "example/1", 22 | }, 23 | }).catch(mock.explain); 24 | 25 | // create success status 26 | await axios({ 27 | method: "post", 28 | url: "https://api.github.com/repos/octokit-fixture-org/create-status/statuses/0000000000000000000000000000000000000001", 29 | headers: { 30 | Accept: "application/vnd.github.v3+json", 31 | Authorization: "token 0000000000000000000000000000000000000001", 32 | "Content-Type": "application/json; charset=utf-8", 33 | }, 34 | data: { 35 | state: "success", 36 | target_url: "https://example.com", 37 | description: "create-status success test", 38 | context: "example/2", 39 | }, 40 | }).catch(mock.explain); 41 | 42 | // get all statuses 43 | await axios({ 44 | method: "get", 45 | url: "https://api.github.com/repos/octokit-fixture-org/create-status/commits/0000000000000000000000000000000000000001/statuses", 46 | headers: { 47 | Accept: "application/vnd.github.v3+json", 48 | Authorization: "token 0000000000000000000000000000000000000001", 49 | }, 50 | }); 51 | 52 | // get combined status 53 | const { data } = await axios({ 54 | method: "get", 55 | url: "https://api.github.com/repos/octokit-fixture-org/create-status/commits/0000000000000000000000000000000000000001/status", 56 | headers: { 57 | Accept: "application/vnd.github.v3+json", 58 | Authorization: "token 0000000000000000000000000000000000000001", 59 | }, 60 | }); 61 | 62 | expect(data.state).toBe("failure"); 63 | expect(mock.done.bind(mock)).not.toThrow(); 64 | }); 65 | -------------------------------------------------------------------------------- /scenarios/api.github.com/search-issues/record.js: -------------------------------------------------------------------------------- 1 | export default searchIssues; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function searchIssues(state) { 6 | let error; 7 | 8 | // create a temporary repository 9 | const temporaryRepository = getTemporaryRepository({ 10 | request: state.request, 11 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 12 | org: "octokit-fixture-org", 13 | name: "search-issues", 14 | }); 15 | 16 | await temporaryRepository.create(); 17 | 18 | try { 19 | // https://developer.github.com/v3/issues/#create-an-issue 20 | // (theses requests get ignored, we need an existing issues for the search) 21 | await state.request({ 22 | method: "post", 23 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/issues`, 24 | headers: { 25 | Accept: "application/vnd.github.v3+json", 26 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 27 | "X-Octokit-Fixture-Ignore": "true", 28 | }, 29 | data: { 30 | title: "The doors don’t open", 31 | body: 'I tried "open sesame" as seen on Wikipedia but no luck!', 32 | }, 33 | }); 34 | 35 | await state.request({ 36 | method: "post", 37 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/issues`, 38 | headers: { 39 | Accept: "application/vnd.github.v3+json", 40 | Authorization: `token ${env.FIXTURES_USER_B_TOKEN_FULL_ACCESS}`, 41 | "X-Octokit-Fixture-Ignore": "true", 42 | }, 43 | data: { 44 | title: "Sesame seeds split without a pop!", 45 | body: "I’ve waited all year long, but there was no pop 😭", 46 | }, 47 | }); 48 | 49 | // timeout for search indexing 50 | await new Promise((resolve) => setTimeout(resolve, 15000)); 51 | 52 | const query = `sesame repo:octokit-fixture-org/${temporaryRepository.name}`; 53 | await state.request({ 54 | method: "get", 55 | url: `/search/issues?q=${encodeURIComponent(query)}`, 56 | headers: { 57 | Accept: "application/vnd.github.v3+json", 58 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 59 | }, 60 | }); 61 | } catch (_error) { 62 | error = _error; 63 | } 64 | 65 | await temporaryRepository.delete(); 66 | 67 | if (error) { 68 | return Promise.reject(error); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/integration/smoke.test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../index.js"; 4 | import { readFileSync } from "fs"; 5 | 6 | test("Accepts fixtures object as argument", async () => { 7 | fixtures.mock( 8 | JSON.parse( 9 | readFileSync( 10 | "./scenarios/api.github.com/get-repository/normalized-fixture.json", 11 | ), 12 | ), 13 | ); 14 | 15 | const result = await axios({ 16 | method: "get", 17 | url: "https://api.github.com/repos/octokit-fixture-org/hello-world", 18 | headers: { 19 | Accept: "application/vnd.github.v3+json", 20 | Authorization: "token 0000000000000000000000000000000000000001", 21 | }, 22 | }); 23 | 24 | expect(result.data.name).toBe("hello-world"); 25 | }); 26 | 27 | test("Missing Accept header", async () => { 28 | fixtures.mock("api.github.com/get-repository"); 29 | 30 | try { 31 | await axios({ 32 | method: "get", 33 | url: "https://api.github.com/repos/octokit-fixture-org/hello-world", 34 | }); 35 | throw new Error("request should fail"); 36 | } catch (error) { 37 | expect(error.message).toMatch("No match for request"); 38 | } 39 | }); 40 | 41 | test("Matches correct fixture based on authorization header", async () => { 42 | fixtures.mock("api.github.com/get-root"); 43 | 44 | const result = await axios({ 45 | method: "get", 46 | url: "https://api.github.com", 47 | headers: { 48 | Accept: "application/vnd.github.v3+json", 49 | Authorization: "token 0000000000000000000000000000000000000001", 50 | }, 51 | }); 52 | 53 | expect(result.headers["x-ratelimit-remaining"]).toBe("4999"); 54 | }); 55 | 56 | test("unmatched request error", async () => { 57 | const mock = fixtures.mock("api.github.com/get-repository"); 58 | 59 | try { 60 | await axios({ 61 | method: "get", 62 | url: "https://api.github.com/unknown", 63 | headers: { 64 | Accept: "application/vnd.github.v3+json", 65 | }, 66 | }).catch(mock.explain); 67 | throw new Error("request should fail"); 68 | } catch (error) { 69 | expect(error.message).toMatch('+ url: "https://api.github.com/unknown'); 70 | } 71 | }); 72 | 73 | test("explain non-request error", async () => { 74 | const mock = fixtures.mock("api.github.com/get-repository"); 75 | 76 | try { 77 | mock.explain(new Error("foo")); 78 | throw new Error("should throw error"); 79 | } catch (error) { 80 | expect(error.message).toBe("foo"); 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@octokit/fixtures", 3 | "version": "0.0.0-development", 4 | "publishConfig": { 5 | "access": "public", 6 | "provenance": true 7 | }, 8 | "description": "Fixtures for all the octokittens", 9 | "main": "index.js", 10 | "type": "module", 11 | "files": [ 12 | "index.js", 13 | "bin", 14 | "lib", 15 | "scenarios" 16 | ], 17 | "scripts": { 18 | "coverage": "tap --coverage-report=html", 19 | "record": "bin/record.js", 20 | "pretest": "npm run -s lint", 21 | "lint": "prettier --check '{bin,lib,scenarios,test}/**/*.{js,json}' index.js README.md package.json", 22 | "lint:fix": "prettier --write '{bin,lib,scenarios,test}/**/*.{js,json}' index.js README.md package.json", 23 | "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --coverage 'scenarios/.*/test.js' 'test/.*/.*.test.js'", 24 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules npx jest -i 'test/unit/.*/*.test.js'", 25 | "test:integration": "NODE_OPTIONS=--experimental-vm-modules npx jest -i 'test/integration/.*/*.test.js'", 26 | "test:scenarios": "NODE_OPTIONS=--experimental-vm-modules npx jest -i 'scenarios/.*/test.js'", 27 | "semantic-release": "semantic-release" 28 | }, 29 | "repository": "github:octokit/fixtures", 30 | "keywords": [], 31 | "author": "Gregor Martynus (https://twitter.com/gr2m)", 32 | "license": "MIT", 33 | "dependencies": { 34 | "json-diff": "^1.0.0", 35 | "lodash": "^4.17.11", 36 | "nock": "^13.0.0", 37 | "url-template": "^3.0.0" 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^30.0.0", 41 | "axios": "^1.0.0", 42 | "axios-debug-log": "^1.0.0", 43 | "bottleneck": "^2.12.0", 44 | "chalk": "^5.0.0", 45 | "envalid": "^8.0.0", 46 | "get-stream": "^9.0.0", 47 | "glob": "^11.0.0", 48 | "gunzip-maybe": "^1.4.1", 49 | "humanize-string": "^3.0.0", 50 | "into-stream": "^9.0.0", 51 | "jest": "^30.0.0", 52 | "minimist": "^1.2.5", 53 | "mkdirp": "^3.0.0", 54 | "prettier": "3.5.3", 55 | "proxyquire": "^2.1.0", 56 | "tar-stream": "^3.0.0" 57 | }, 58 | "release": { 59 | "branches": [ 60 | "+([0-9]).x", 61 | "main", 62 | "next", 63 | { 64 | "name": "beta", 65 | "prerelease": true 66 | } 67 | ] 68 | }, 69 | "jest": { 70 | "coverageThreshold": { 71 | "global": { 72 | "branches": 100, 73 | "functions": 100, 74 | "lines": 100, 75 | "statements": 100 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /scenarios/api.github.com/rename-repository/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Labels", async () => { 6 | const mock = fixtures.mock("api.github.com/rename-repository"); 7 | 8 | // https://developer.github.com/v3/repos/#edit 9 | await axios({ 10 | method: "patch", 11 | url: "https://api.github.com/repos/octokit-fixture-org/rename-repository", 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | Authorization: "token 0000000000000000000000000000000000000001", 15 | "Content-Type": "application/json; charset=utf-8", 16 | }, 17 | data: { 18 | name: "rename-repository-newname", 19 | }, 20 | }).catch(mock.explain); 21 | 22 | // https://developer.github.com/v3/repos/#get 23 | await axios({ 24 | method: "get", 25 | url: "https://api.github.com/repos/octokit-fixture-org/rename-repository", 26 | headers: { 27 | Accept: "application/vnd.github.v3+json", 28 | Authorization: "token 0000000000000000000000000000000000000001", 29 | }, 30 | }).catch(mock.explain); 31 | 32 | // https://developer.github.com/v3/repos/#edit 33 | await axios({ 34 | method: "patch", 35 | url: "https://api.github.com/repos/octokit-fixture-org/rename-repository", 36 | headers: { 37 | Accept: "application/vnd.github.v3+json", 38 | Authorization: "token 0000000000000000000000000000000000000001", 39 | "Content-Type": "application/json; charset=utf-8", 40 | }, 41 | data: { 42 | name: "rename-repository-newname", 43 | description: "test description", 44 | }, 45 | // axios (or the lower level follow-redirects package) does not handle 307 46 | // redirects correctly 47 | maxRedirects: 0, 48 | }) 49 | .catch((error) => { 50 | if (error.response.status === 307) { 51 | return; // all good 52 | } 53 | 54 | throw error; 55 | }) 56 | .catch(mock.explain); 57 | 58 | // https://developer.github.com/v3/repos/#edit 59 | await axios({ 60 | method: "patch", 61 | url: "https://api.github.com/repositories/1000", 62 | headers: { 63 | Accept: "application/vnd.github.v3+json", 64 | Authorization: "token 0000000000000000000000000000000000000001", 65 | "Content-Type": "application/json; charset=utf-8", 66 | }, 67 | data: { 68 | name: "rename-repository-newname", 69 | description: "test description", 70 | }, 71 | }).catch(mock.explain); 72 | 73 | expect(mock.done.bind(mock)).not.toThrow(); 74 | }); 75 | -------------------------------------------------------------------------------- /lib/fixturize-path.js: -------------------------------------------------------------------------------- 1 | export default fixturizePath; 2 | 3 | import fixturizeCommitSha from "./fixturize-commit-sha.js"; 4 | import fixturizeEntityId from "./fixturize-entity-id.js"; 5 | import { regex } from "./temporary-repository.js"; 6 | 7 | const PLACEHOLDER_REGEX = /\{[^}]+\}/g; 8 | const PATH_TEMPLATES = [ 9 | "/cards/{project-card}", 10 | "/columns/{project-column}", 11 | "/commit/{commit-sha}", 12 | "/commits/{commit-sha}", 13 | "/git/trees/{commit-sha}", 14 | "/projects/{project}", 15 | "/releases/{release}", 16 | "/releases/assets/{release-asset}", 17 | "/repositories/{repository}", 18 | "/repository_invitations/{invitation}", 19 | "/statuses/{commit-sha}", 20 | "/teams/{team}", 21 | "/u/{owner}", 22 | ]; 23 | // sha hashes are always 40 characters long and can contain numbers or letters 24 | // IDs are always numeric. We check for a sha hash first, then fallback to numeric id 25 | const FIND_SHA_OR_ID_REGEX_STRING = "(\\w{40}|\\d+)"; 26 | 27 | function fixturizePath(scenarioState, path) { 28 | // rename temporary repositories 29 | // e.g. tmp-scenario-bar-20170924033013835 => bar 30 | path = path.replace(regex, "$1"); 31 | 32 | const pathTemplate = PATH_TEMPLATES.find((pathTemplate) => { 33 | const regexString = pathTemplate.replace( 34 | PLACEHOLDER_REGEX, 35 | FIND_SHA_OR_ID_REGEX_STRING, 36 | ); 37 | return new RegExp(regexString).test(path); 38 | }); 39 | 40 | if (!pathTemplate) { 41 | return path; 42 | } 43 | 44 | const placeholderRegex = new RegExp( 45 | pathTemplate.replace(PLACEHOLDER_REGEX, FIND_SHA_OR_ID_REGEX_STRING), 46 | ); 47 | const placeholderNames = pathTemplate.match(PLACEHOLDER_REGEX); 48 | const idsOrShas = path.match(placeholderRegex).slice(1); 49 | 50 | const newPath = placeholderNames.reduce((pathTemplate, placeholder, i) => { 51 | const entityName = toEntityName(placeholder); 52 | 53 | if (entityName === "commit-sha") { 54 | return pathTemplate.replace( 55 | placeholder, 56 | fixturizeCommitSha(scenarioState.commitSha, idsOrShas[i]), 57 | ); 58 | } 59 | 60 | return pathTemplate.replace( 61 | placeholder, 62 | fixturizeEntityId(scenarioState.ids, entityName, idsOrShas[i]), 63 | ); 64 | }, pathTemplate); 65 | 66 | return ( 67 | path 68 | .replace(placeholderRegex, newPath) 69 | // ?u=[sha] query parameter 70 | .replace(/\?u=\w{40}/, "?u=0000000000000000000000000000000000000001") 71 | ); 72 | } 73 | 74 | function toEntityName(placeholder) { 75 | return placeholder.replace(/[{}]/g, ""); 76 | } 77 | -------------------------------------------------------------------------------- /lib/normalize/branch-protection.js: -------------------------------------------------------------------------------- 1 | export default branchProtection; 2 | 3 | import get from "lodash/get.js"; 4 | 5 | import normalizeTeam from "./team.js"; 6 | import normalizeUser from "./user.js"; 7 | import setIfExists from "../set-if-exists.js"; 8 | import { regex } from "../temporary-repository.js"; 9 | 10 | // https://developer.github.com/v3/repos/branches/#response-2 11 | function branchProtection(scenarioState, response) { 12 | // normalize temporary repository 13 | setIfExists(response, "url", (url) => url.replace(regex, "$1")); 14 | setIfExists(response, "required_status_checks.url", (url) => 15 | url.replace(regex, "$1"), 16 | ); 17 | setIfExists(response, "required_status_checks.contexts_url", (url) => 18 | url.replace(regex, "$1"), 19 | ); 20 | setIfExists(response, "required_pull_request_reviews.url", (url) => 21 | url.replace(regex, "$1"), 22 | ); 23 | setIfExists( 24 | response, 25 | "required_pull_request_reviews.dismissal_restrictions.url", 26 | (url) => url.replace(regex, "$1"), 27 | ); 28 | setIfExists( 29 | response, 30 | "required_pull_request_reviews.dismissal_restrictions.users_url", 31 | (url) => url.replace(regex, "$1"), 32 | ); 33 | setIfExists( 34 | response, 35 | "required_pull_request_reviews.dismissal_restrictions.teams_url", 36 | (url) => url.replace(regex, "$1"), 37 | ); 38 | setIfExists(response, "enforce_admins.url", (url) => 39 | url.replace(regex, "$1"), 40 | ); 41 | setIfExists(response, "restrictions.url", (url) => url.replace(regex, "$1")); 42 | setIfExists(response, "restrictions.users_url", (url) => 43 | url.replace(regex, "$1"), 44 | ); 45 | setIfExists(response, "restrictions.teams_url", (url) => 46 | url.replace(regex, "$1"), 47 | ); 48 | setIfExists(response, "restrictions.apps_url", (url) => 49 | url.replace(regex, "$1"), 50 | ); 51 | 52 | // normalize users 53 | const dismissalRestrictionsUsers = 54 | get( 55 | response, 56 | "required_pull_request_reviews.dismissal_restrictions.users", 57 | ) || []; 58 | const dismissalRestrictionsTeams = 59 | get( 60 | response, 61 | "required_pull_request_reviews.dismissal_restrictions.teams", 62 | ) || []; 63 | const restrictionsUsers = get(response, "restrictions.users") || []; 64 | const restrictionsTeams = get(response, "restrictions.teams") || []; 65 | 66 | dismissalRestrictionsUsers 67 | .concat(restrictionsUsers) 68 | .forEach(normalizeUser.bind(null, scenarioState)); 69 | dismissalRestrictionsTeams 70 | .concat(restrictionsTeams) 71 | .forEach(normalizeTeam.bind(null, scenarioState)); 72 | } 73 | -------------------------------------------------------------------------------- /scenarios/api.github.com/labels/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Labels", async () => { 6 | const mock = fixtures.mock("api.github.com/labels"); 7 | 8 | // https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository 9 | // List all labels for a repository 10 | await axios({ 11 | method: "get", 12 | url: "https://api.github.com/repos/octokit-fixture-org/labels/labels", 13 | headers: { 14 | Accept: "application/vnd.github.v3+json", 15 | Authorization: "token 0000000000000000000000000000000000000001", 16 | }, 17 | }).catch(mock.explain); 18 | 19 | // https://developer.github.com/v3/issues/labels/#create-a-label 20 | // Create a label 21 | await axios({ 22 | method: "post", 23 | url: "https://api.github.com/repos/octokit-fixture-org/labels/labels", 24 | headers: { 25 | Accept: "application/vnd.github.v3+json", 26 | Authorization: "token 0000000000000000000000000000000000000001", 27 | "Content-Type": "application/json; charset=utf-8", 28 | }, 29 | data: { 30 | name: "test-label", 31 | color: "663399", 32 | }, 33 | }).catch(mock.explain); 34 | 35 | // https://developer.github.com/v3/issues/labels/#get-a-single-label 36 | // Get a label 37 | await axios({ 38 | method: "get", 39 | url: "https://api.github.com/repos/octokit-fixture-org/labels/labels/test-label", 40 | headers: { 41 | Accept: "application/vnd.github.v3+json", 42 | Authorization: "token 0000000000000000000000000000000000000001", 43 | }, 44 | }).catch(mock.explain); 45 | 46 | // https://developer.github.com/v3/issues/labels/#get-a-single-label 47 | // Update a label 48 | await axios({ 49 | method: "patch", 50 | url: "https://api.github.com/repos/octokit-fixture-org/labels/labels/test-label", 51 | headers: { 52 | Accept: "application/vnd.github.v3+json", 53 | Authorization: "token 0000000000000000000000000000000000000001", 54 | "Content-Type": "application/json; charset=utf-8", 55 | }, 56 | data: { 57 | new_name: "test-label-updated", 58 | color: "BADA55", 59 | }, 60 | }).catch(mock.explain); 61 | 62 | // https://developer.github.com/v3/issues/labels/#delete-a-label 63 | // Delete a label 64 | await axios({ 65 | method: "delete", 66 | url: "https://api.github.com/repos/octokit-fixture-org/labels/labels/test-label-updated", 67 | headers: { 68 | Accept: "application/vnd.github.v3+json", 69 | Authorization: "token 0000000000000000000000000000000000000001", 70 | }, 71 | }).catch(mock.explain); 72 | 73 | expect(mock.done.bind(mock)).not.toThrow(); 74 | }); 75 | -------------------------------------------------------------------------------- /scenarios/api.github.com/git-refs/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Git references", async () => { 6 | const mock = fixtures.mock("api.github.com/git-refs"); 7 | 8 | // https://developer.github.com/v3/git/refs/#get-all-references 9 | // returns a single reference for the master branch, pointing to sha for 2nd commit 10 | await axios({ 11 | method: "get", 12 | url: "https://api.github.com/repos/octokit-fixture-org/git-refs/git/refs/", 13 | headers: { 14 | Accept: "application/vnd.github.v3+json", 15 | Authorization: "token 0000000000000000000000000000000000000001", 16 | }, 17 | }).catch(mock.explain); 18 | 19 | // https://developer.github.com/v3/git/refs/#create-a-reference 20 | // Create a new branch "test" pointing to sha of initial commit 21 | await axios({ 22 | method: "post", 23 | url: "https://api.github.com/repos/octokit-fixture-org/git-refs/git/refs", 24 | headers: { 25 | Accept: "application/vnd.github.v3+json", 26 | Authorization: "token 0000000000000000000000000000000000000001", 27 | "Content-Type": "application/json; charset=utf-8", 28 | }, 29 | data: { 30 | ref: "refs/heads/test", 31 | sha: "0000000000000000000000000000000000000002", 32 | }, 33 | }).catch(mock.explain); 34 | 35 | // https://developer.github.com/v3/git/refs/#update-a-reference 36 | // update test branch to point to sha of 2nd commit instead 37 | await axios({ 38 | method: "patch", 39 | url: "https://api.github.com/repos/octokit-fixture-org/git-refs/git/refs/heads/test", 40 | headers: { 41 | Accept: "application/vnd.github.v3+json", 42 | Authorization: "token 0000000000000000000000000000000000000001", 43 | "Content-Type": "application/json; charset=utf-8", 44 | }, 45 | data: { 46 | sha: "0000000000000000000000000000000000000001", 47 | }, 48 | }).catch(mock.explain); 49 | 50 | // https://developer.github.com/v3/git/refs/#get-all-references 51 | // Now returns both branches: master & test 52 | await axios({ 53 | method: "get", 54 | url: "https://api.github.com/repos/octokit-fixture-org/git-refs/git/refs/", 55 | headers: { 56 | Accept: "application/vnd.github.v3+json", 57 | Authorization: "token 0000000000000000000000000000000000000001", 58 | }, 59 | }).catch(mock.explain); 60 | 61 | // https://developer.github.com/v3/git/refs/#delete-a-reference 62 | // Delete test branch 63 | await axios({ 64 | method: "delete", 65 | url: "https://api.github.com/repos/octokit-fixture-org/git-refs/git/refs/heads/test", 66 | headers: { 67 | Accept: "application/vnd.github.v3+json", 68 | Authorization: "token 0000000000000000000000000000000000000001", 69 | }, 70 | }).catch(mock.explain); 71 | 72 | expect(mock.done.bind(mock)).not.toThrow(); 73 | }); 74 | -------------------------------------------------------------------------------- /scenarios/api.github.com/release-assets-conflict/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Labels", async () => { 6 | expect.assertions(2); 7 | const mock = fixtures.mock("api.github.com/release-assets-conflict"); 8 | 9 | // https://developer.github.com/v3/repos/releases/#upload-a-release-asset 10 | // Get release to retrieve upload URL 11 | await axios({ 12 | method: "get", 13 | url: "https://api.github.com/repos/octokit-fixture-org/release-assets-conflict/releases/tags/v1.0.0", 14 | headers: { 15 | Accept: "application/vnd.github.v3+json", 16 | Authorization: "token 0000000000000000000000000000000000000001", 17 | }, 18 | }); 19 | 20 | // https://developer.github.com/v3/repos/releases/#upload-a-release-asset 21 | // upload attachment to release URL returned by create release request 22 | try { 23 | await axios({ 24 | method: "post", 25 | url: "https://uploads.github.com/repos/octokit-fixture-org/release-assets-conflict/releases/1000/assets?name=test-upload.txt&label=test", 26 | headers: { 27 | Accept: "application/vnd.github.v3+json", 28 | Authorization: "token 0000000000000000000000000000000000000001", 29 | "Content-Type": "text/plain", 30 | "Content-Length": 14, 31 | }, 32 | data: "Hello, world!\n", 33 | }); 34 | } catch (error) { 35 | expect(error.response.status).toBe(422); 36 | } 37 | 38 | // https://developer.github.com/v3/repos/releases/#list-assets-for-a-release 39 | // Retrieve the asset lists to get the id of the existing asset 40 | await axios({ 41 | method: "get", 42 | url: "https://api.github.com/repos/octokit-fixture-org/release-assets-conflict/releases/1000/assets", 43 | headers: { 44 | Accept: "application/vnd.github.v3+json", 45 | Authorization: "token 0000000000000000000000000000000000000001", 46 | }, 47 | }); 48 | 49 | // https://developer.github.com/v3/repos/releases/#delete-a-release-asset 50 | // delete the existing asset 51 | await axios({ 52 | method: "delete", 53 | url: "https://api.github.com/repos/octokit-fixture-org/release-assets-conflict/releases/assets/1000", 54 | headers: { 55 | Accept: "application/vnd.github.v3+json", 56 | Authorization: "token 0000000000000000000000000000000000000001", 57 | }, 58 | }); 59 | 60 | // Upload again, this time it will work 61 | await axios({ 62 | method: "post", 63 | url: "https://uploads.github.com/repos/octokit-fixture-org/release-assets-conflict/releases/1000/assets?name=test-upload.txt&label=test", 64 | headers: { 65 | Accept: "application/vnd.github.v3+json", 66 | Authorization: "token 0000000000000000000000000000000000000001", 67 | "Content-Type": "text/plain", 68 | "Content-Length": 14, 69 | }, 70 | data: "Hello, world!\n", 71 | }); 72 | 73 | expect(mock.done.bind(mock)).not.toThrow(); 74 | }); 75 | -------------------------------------------------------------------------------- /lib/normalize/repository.js: -------------------------------------------------------------------------------- 1 | export default normalizeRepository; 2 | 3 | import fixturizeEntityId from "../fixturize-entity-id.js"; 4 | import normalizeOrganization from "./organization.js"; 5 | import normalizeUser from "./user.js"; 6 | import setIfExists from "../set-if-exists.js"; 7 | import { regex } from "../temporary-repository.js"; 8 | 9 | function normalizeRepository(scenarioState, response, fixture) { 10 | // set all IDs to 1 11 | setIfExists( 12 | response, 13 | "id", 14 | fixturizeEntityId.bind(null, scenarioState.ids, "repository"), 15 | ); 16 | 17 | // set all dates to Universe 2017 Keynote time 18 | setIfExists(response, "created_at", "2017-10-10T16:00:00Z"); 19 | setIfExists(response, "updated_at", "2017-10-10T16:00:00Z"); 20 | setIfExists(response, "pushed_at", "2017-10-10T16:00:00Z"); 21 | 22 | // set all counts to 42 23 | setIfExists(response, "forks_count", 42); 24 | setIfExists(response, "forks", 42); 25 | setIfExists(response, "network_count", 42); 26 | setIfExists(response, "open_issues_count", 42); 27 | setIfExists(response, "open_issues", 42); 28 | setIfExists(response, "stargazers_count", 42); 29 | setIfExists(response, "subscribers_count", 42); 30 | setIfExists(response, "watchers_count", 42); 31 | setIfExists(response, "watchers", 42); 32 | 33 | // normalize temporary repository 34 | [ 35 | "name", 36 | "full_name", 37 | "html_url", 38 | "url", 39 | "forks_url", 40 | "keys_url", 41 | "collaborators_url", 42 | "teams_url", 43 | "hooks_url", 44 | "issue_events_url", 45 | "events_url", 46 | "assignees_url", 47 | "branches_url", 48 | "tags_url", 49 | "blobs_url", 50 | "git_tags_url", 51 | "git_refs_url", 52 | "trees_url", 53 | "statuses_url", 54 | "languages_url", 55 | "stargazers_url", 56 | "contributors_url", 57 | "subscribers_url", 58 | "subscription_url", 59 | "commits_url", 60 | "git_commits_url", 61 | "comments_url", 62 | "issue_comment_url", 63 | "contents_url", 64 | "compare_url", 65 | "merges_url", 66 | "archive_url", 67 | "downloads_url", 68 | "issues_url", 69 | "pulls_url", 70 | "milestones_url", 71 | "notifications_url", 72 | "labels_url", 73 | "releases_url", 74 | "deployments_url", 75 | "git_url", 76 | "ssh_url", 77 | "clone_url", 78 | "svn_url", 79 | ].forEach((property) => { 80 | // not all these properties are set in repository response all the time 81 | setIfExists(response, property, (value) => { 82 | return value.replace(regex, "$1"); 83 | }); 84 | }); 85 | 86 | if (response.organization) { 87 | normalizeOrganization(scenarioState, response.owner); 88 | normalizeOrganization(scenarioState, response.organization); 89 | } else { 90 | normalizeUser(scenarioState, response.owner); 91 | } 92 | 93 | // normalize payload 94 | setIfExists(fixture, "body.name", (name) => name.replace(regex, "$1")); 95 | } 96 | -------------------------------------------------------------------------------- /scenarios/api.github.com/labels/record.js: -------------------------------------------------------------------------------- 1 | export default labels; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function labels(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "labels", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository 19 | // List all labels for a repository 20 | await state.request({ 21 | method: "get", 22 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/labels`, 23 | headers: { 24 | Accept: "application/vnd.github.v3+json", 25 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 26 | }, 27 | }); 28 | 29 | // https://developer.github.com/v3/issues/labels/#create-a-label 30 | // Create a label 31 | await state.request({ 32 | method: "post", 33 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/labels`, 34 | headers: { 35 | Accept: "application/vnd.github.v3+json", 36 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 37 | }, 38 | data: { 39 | name: "test-label", 40 | color: "663399", 41 | }, 42 | }); 43 | 44 | // https://developer.github.com/v3/issues/labels/#get-a-single-label 45 | // Get a label 46 | await state.request({ 47 | method: "get", 48 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/labels/test-label`, 49 | headers: { 50 | Accept: "application/vnd.github.v3+json", 51 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 52 | }, 53 | }); 54 | 55 | // https://developer.github.com/v3/issues/labels/#get-a-single-label 56 | // Update a label 57 | await state.request({ 58 | method: "patch", 59 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/labels/test-label`, 60 | headers: { 61 | Accept: "application/vnd.github.v3+json", 62 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 63 | }, 64 | data: { 65 | new_name: "test-label-updated", 66 | color: "BADA55", 67 | }, 68 | }); 69 | 70 | // https://developer.github.com/v3/issues/labels/#delete-a-label 71 | // Delete a label 72 | await state.request({ 73 | method: "delete", 74 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/labels/test-label-updated`, 75 | headers: { 76 | Accept: "application/vnd.github.v3+json", 77 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 78 | }, 79 | }); 80 | } catch (_error) { 81 | error = _error; 82 | } 83 | 84 | await temporaryRepository.delete(); 85 | 86 | if (error) { 87 | return Promise.reject(error); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /scenarios/api.github.com/add-and-remove-repository-collaborator/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Get repository", async () => { 6 | const mock = fixtures.mock( 7 | "api.github.com/add-and-remove-repository-collaborator", 8 | ); 9 | 10 | // https://developer.github.com/v3/repos/collaborators/#add-user-as-a-collaborator 11 | await axios({ 12 | method: "put", 13 | url: "https://api.github.com/repos/octokit-fixture-org/add-and-remove-repository-collaborator/collaborators/octokit-fixture-user-b", 14 | headers: { 15 | Accept: "application/vnd.github.v3+json", 16 | Authorization: "token 0000000000000000000000000000000000000001", 17 | "Content-Length": 0, 18 | }, 19 | }).catch(mock.explain); 20 | 21 | // https://developer.github.com/v3/repos/invitations/ 22 | const invitationsResponse = await axios({ 23 | method: "get", 24 | url: "https://api.github.com/repos/octokit-fixture-org/add-and-remove-repository-collaborator/invitations", 25 | headers: { 26 | Accept: "application/vnd.github.v3+json", 27 | Authorization: "token 0000000000000000000000000000000000000001", 28 | }, 29 | }).catch(mock.explain); 30 | 31 | expect(invitationsResponse.data[0].id).toBe(1000); 32 | 33 | // https://developer.github.com/v3/repos/invitations/#accept-a-repository-invitation 34 | await axios({ 35 | method: "patch", 36 | url: "https://api.github.com/user/repository_invitations/1000", 37 | headers: { 38 | Accept: "application/vnd.github.v3+json", 39 | Authorization: "token 0000000000000000000000000000000000000002", 40 | "Content-Length": 0, 41 | }, 42 | }).catch(mock.explain); 43 | 44 | // https://developer.github.com/v3/repos/collaborators/#list-collaborators 45 | await axios({ 46 | method: "get", 47 | url: "https://api.github.com/repos/octokit-fixture-org/add-and-remove-repository-collaborator/collaborators", 48 | headers: { 49 | Accept: "application/vnd.github.v3+json", 50 | Authorization: "token 0000000000000000000000000000000000000001", 51 | }, 52 | }).catch(mock.explain); 53 | 54 | // https://developer.github.com/v3/repos/collaborators/#remove-user-as-a-collaborator 55 | await axios({ 56 | method: "delete", 57 | url: "https://api.github.com/repos/octokit-fixture-org/add-and-remove-repository-collaborator/collaborators/octokit-fixture-user-b", 58 | headers: { 59 | Accept: "application/vnd.github.v3+json", 60 | Authorization: "token 0000000000000000000000000000000000000001", 61 | }, 62 | }).catch(mock.explain); 63 | 64 | // https://developer.github.com/v3/repos/collaborators/#list-collaborators 65 | await axios({ 66 | method: "get", 67 | url: "https://api.github.com/repos/octokit-fixture-org/add-and-remove-repository-collaborator/collaborators", 68 | headers: { 69 | Accept: "application/vnd.github.v3+json", 70 | Authorization: "token 0000000000000000000000000000000000000001", 71 | }, 72 | }).catch(mock.explain); 73 | 74 | expect(mock.done.bind(mock)).not.toThrow(); 75 | }); 76 | -------------------------------------------------------------------------------- /scenarios/api.github.com/release-assets/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Labels", async () => { 6 | const mock = fixtures.mock("api.github.com/release-assets"); 7 | 8 | // https://developer.github.com/v3/repos/releases/#upload-a-release-asset 9 | // Get release to retrieve upload URL 10 | await axios({ 11 | method: "get", 12 | url: "https://api.github.com/repos/octokit-fixture-org/release-assets/releases/tags/v1.0.0", 13 | headers: { 14 | Accept: "application/vnd.github.v3+json", 15 | Authorization: "token 0000000000000000000000000000000000000001", 16 | }, 17 | }); 18 | 19 | // https://developer.github.com/v3/repos/releases/#upload-a-release-asset 20 | // upload attachment to release URL returned by create release request 21 | await axios({ 22 | method: "post", 23 | url: "https://uploads.github.com/repos/octokit-fixture-org/release-assets/releases/1000/assets?name=test-upload.txt&label=test", 24 | headers: { 25 | Accept: "application/vnd.github.v3+json", 26 | Authorization: "token 0000000000000000000000000000000000000001", 27 | "Content-Type": "text/plain", 28 | "Content-Length": 14, 29 | }, 30 | data: "Hello, world!\n", 31 | }); 32 | 33 | // https://developer.github.com/v3/repos/releases/#list-assets-for-a-release 34 | // list assets for release 35 | await axios({ 36 | method: "get", 37 | url: "https://api.github.com/repos/octokit-fixture-org/release-assets/releases/1000/assets", 38 | headers: { 39 | Accept: "application/vnd.github.v3+json", 40 | Authorization: "token 0000000000000000000000000000000000000001", 41 | }, 42 | }); 43 | 44 | // https://developer.github.com/v3/repos/releases/#get-a-single-release-asset 45 | // get single release asset 46 | await axios({ 47 | method: "get", 48 | url: "https://api.github.com/repos/octokit-fixture-org/release-assets/releases/assets/1000", 49 | headers: { 50 | Accept: "application/vnd.github.v3+json", 51 | Authorization: "token 0000000000000000000000000000000000000001", 52 | }, 53 | }); 54 | 55 | // https://developer.github.com/v3/repos/releases/#get-a-single-release-asset 56 | // Edit name / label of release asset 57 | await axios({ 58 | method: "patch", 59 | url: "https://api.github.com/repos/octokit-fixture-org/release-assets/releases/assets/1000", 60 | headers: { 61 | Accept: "application/vnd.github.v3+json", 62 | Authorization: "token 0000000000000000000000000000000000000001", 63 | "Content-Type": "application/json; charset=utf-8", 64 | }, 65 | data: { 66 | name: "new-filename.txt", 67 | label: "new label", 68 | }, 69 | }); 70 | 71 | // https://developer.github.com/v3/repos/releases/#delete-a-release-asset 72 | // delete a release asset 73 | await axios({ 74 | method: "delete", 75 | url: "https://api.github.com/repos/octokit-fixture-org/release-assets/releases/assets/1000", 76 | headers: { 77 | Accept: "application/vnd.github.v3+json", 78 | Authorization: "token 0000000000000000000000000000000000000001", 79 | }, 80 | }); 81 | 82 | expect(mock.done.bind(mock)).not.toThrow(); 83 | }); 84 | -------------------------------------------------------------------------------- /scenarios/api.github.com/branch-protection/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import fixtures from "../../../index.js"; 3 | 4 | test("Branch protection", async () => { 5 | const mock = fixtures.mock("api.github.com/branch-protection"); 6 | 7 | // https://developer.github.com/v3/repos/branches/#get-branch-protection 8 | // Get branch protection 9 | await axios({ 10 | method: "get", 11 | url: "https://api.github.com/repos/octokit-fixture-org/branch-protection/branches/main/protection", 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | Authorization: "token 0000000000000000000000000000000000000001", 15 | }, 16 | }) 17 | .catch((error) => { 18 | if (error.response.data.message === "Branch not protected") { 19 | return; // this 404 is expected 20 | } 21 | 22 | throw error; 23 | }) 24 | .catch(mock.explain); 25 | 26 | // https://developer.github.com/v3/repos/branches/#update-branch-protection 27 | // Update branch protection with minimal settings 28 | await axios({ 29 | method: "put", 30 | url: "https://api.github.com/repos/octokit-fixture-org/branch-protection/branches/main/protection", 31 | headers: { 32 | Accept: "application/vnd.github.v3+json", 33 | Authorization: "token 0000000000000000000000000000000000000001", 34 | "Content-Type": "application/json; charset=utf-8", 35 | }, 36 | data: { 37 | required_status_checks: null, 38 | required_pull_request_reviews: null, 39 | restrictions: null, 40 | enforce_admins: false, 41 | }, 42 | }).catch(mock.explain); 43 | 44 | // https://developer.github.com/v3/repos/branches/#update-branch-protection 45 | // Update branch protection with maximal settings 46 | await axios({ 47 | method: "put", 48 | url: "https://api.github.com/repos/octokit-fixture-org/branch-protection/branches/main/protection", 49 | headers: { 50 | Accept: "application/vnd.github.v3+json", 51 | Authorization: "token 0000000000000000000000000000000000000001", 52 | "Content-Type": "application/json; charset=utf-8", 53 | }, 54 | data: { 55 | required_status_checks: { 56 | strict: true, 57 | contexts: ["foo/bar"], 58 | }, 59 | required_pull_request_reviews: { 60 | dismissal_restrictions: { 61 | users: ["octokit-fixture-user-a"], 62 | teams: [], // bug: server returns "Only 100 users and teams can be specified." when set to ['a-team'] 63 | }, 64 | dismiss_stale_reviews: true, 65 | require_code_owner_reviews: false, 66 | }, 67 | restrictions: { 68 | users: ["octokit-fixture-user-a"], 69 | teams: ["a-team"], 70 | }, 71 | enforce_admins: true, 72 | }, 73 | }).catch(mock.explain); 74 | 75 | // https://developer.github.com/v3/repos/branches/#remove-branch-protection 76 | // Remove branch protection 77 | await axios({ 78 | method: "delete", 79 | url: "https://api.github.com/repos/octokit-fixture-org/branch-protection/branches/main/protection", 80 | headers: { 81 | Accept: "application/vnd.github.v3+json", 82 | Authorization: "token 0000000000000000000000000000000000000001", 83 | }, 84 | }).catch(mock.explain); 85 | 86 | expect(mock.done.bind(mock)).not.toThrow(); 87 | }); 88 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource+octokit@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /scenarios/api.github.com/rename-repository/record.js: -------------------------------------------------------------------------------- 1 | export default renameRepository; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function renameRepository(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "rename-repository", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/repos/#edit 19 | // rename repository 20 | await state.request({ 21 | method: "patch", 22 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}`, 23 | headers: { 24 | Accept: "application/vnd.github.v3+json", 25 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 26 | }, 27 | data: { 28 | name: `${temporaryRepository.name}-newname`, 29 | }, 30 | }); 31 | 32 | // wait for 1000ms to account for replication delay 33 | await new Promise((resolve) => setTimeout(resolve, 1000)); 34 | 35 | // https://developer.github.com/v3/repos/#get 36 | // get repository using previous name 37 | await state 38 | .request({ 39 | method: "get", 40 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}`, 41 | headers: { 42 | Accept: "application/vnd.github.v3+json", 43 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 44 | }, 45 | }) 46 | 47 | .catch((error) => { 48 | const newUrl = error.response.data.url; 49 | 50 | // get repository at returned URL 51 | return state.request({ 52 | method: "get", 53 | url: newUrl, 54 | headers: { 55 | Accept: "application/vnd.github.v3+json", 56 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 57 | }, 58 | }); 59 | }); 60 | 61 | // https://developer.github.com/v3/repos/#edit 62 | // update repository using previous name 63 | await state 64 | .request({ 65 | method: "patch", 66 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}`, 67 | headers: { 68 | Accept: "application/vnd.github.v3+json", 69 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 70 | }, 71 | data: { 72 | name: `${temporaryRepository.name}-newname`, 73 | description: "test description", 74 | }, 75 | }) 76 | 77 | .catch((error) => { 78 | const newUrl = error.response.data.url; 79 | 80 | // get repository at returned URL 81 | return state.request({ 82 | method: "patch", 83 | url: newUrl, 84 | headers: { 85 | Accept: "application/vnd.github.v3+json", 86 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 87 | }, 88 | data: { 89 | name: `${temporaryRepository.name}-newname`, 90 | description: "test description", 91 | }, 92 | }); 93 | }); 94 | } catch (_error) { 95 | error = _error; 96 | } 97 | 98 | await temporaryRepository.delete(); 99 | 100 | if (error) { 101 | return Promise.reject(error); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { URL } from "url"; 3 | import cloneDeep from "lodash/cloneDeep.js"; 4 | import merge from "lodash/merge.js"; 5 | import pick from "lodash/pick.js"; 6 | import nock from "nock"; 7 | import headers from "./lib/headers.js"; 8 | import { diffString } from "json-diff"; 9 | import { readFileSync } from "fs"; 10 | 11 | export default { 12 | get, 13 | mock, 14 | nock, 15 | }; 16 | 17 | function get(name) { 18 | return JSON.parse( 19 | readFileSync( 20 | new URL(`./scenarios/${name}/normalized-fixture.json`, import.meta.url), 21 | ).toString(), 22 | ); 23 | } 24 | 25 | function mock(fixtures, additions) { 26 | if (typeof fixtures === "string") { 27 | fixtures = get(fixtures); 28 | } 29 | fixtures = cloneDeep(fixtures); 30 | 31 | if (additions) { 32 | const applyAdditions = 33 | typeof additions === "function" 34 | ? additions 35 | : applyAdditionsDefault.bind(null, additions); 36 | fixtures.forEach((fixture, i) => { 37 | fixtures[i] = applyAdditions(fixture); 38 | }); 39 | } 40 | 41 | fixtures.forEach((fixture) => { 42 | fixture.rawHeaders = headers.toArray(fixture.headers); 43 | delete fixture.headers; 44 | }); 45 | 46 | const mocks = nock.define(fixtures); 47 | 48 | const api = { 49 | pending() { 50 | return [].concat(...mocks.map((mock) => mock.pendingMocks())); 51 | }, 52 | explain(error) { 53 | if (!/^Nock: No match/.test(error.message)) { 54 | throw error; 55 | } 56 | 57 | const expected = getNextMockConfig(mocks); 58 | const actualString = error.message 59 | .substr("Nock: No match for request ".length) 60 | .replace(/\s+Got instead(.|[\r\n])*$/, ""); 61 | 62 | const requestConfig = JSON.parse(actualString); 63 | const actual = pick(requestConfig, Object.keys(expected)); 64 | actual.headers = pick( 65 | requestConfig.headers, 66 | Object.keys(expected.headers), 67 | ); 68 | error.message = `Request did not match mock ${ 69 | api.pending()[0] 70 | }:\n${diffString(expected, actual)}`; 71 | 72 | delete error.config; 73 | delete error.request; 74 | delete error.response; 75 | delete error.status; 76 | delete error.statusCode; 77 | delete error.source; 78 | 79 | throw error; 80 | }, 81 | done() { 82 | assert.ok( 83 | api.isDone(), 84 | `Mocks not yet satisfied:\n${api.pending().join("\n")}`, 85 | ); 86 | }, 87 | isDone() { 88 | return api.pending().length === 0; 89 | }, 90 | }; 91 | 92 | return api; 93 | } 94 | 95 | function getNextMockConfig(mocks) { 96 | const nextMock = mocks.find((mock) => mock.pendingMocks().length > 0) 97 | .interceptors[0]; 98 | return { 99 | method: nextMock.method.toLowerCase(), 100 | url: `https://api.github.com${nextMock.uri}`, 101 | headers: nextMock.options.reqheaders, 102 | }; 103 | } 104 | 105 | function applyAdditionsDefault(additions, fixture) { 106 | merge(fixture, additions); 107 | if (additions.scope) { 108 | const url = new URL(additions.scope); 109 | fixture.reqheaders.host = url.host; 110 | if (fixture.headers.location) { 111 | fixture.headers.location = fixture.headers.location.replace( 112 | "https://api.github.com/", 113 | url.href, 114 | ); 115 | } 116 | } 117 | 118 | return fixture; 119 | } 120 | -------------------------------------------------------------------------------- /scenarios/api.github.com/add-and-remove-repository-collaborator/record.js: -------------------------------------------------------------------------------- 1 | export default addAndRemoveRepositoryCollaborator; 2 | 3 | import env from "../../../lib/env.js"; 4 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 5 | 6 | // - As user A, invite user B as collaborator to repository "octokit-fixture-org/hello-world" 7 | // - As user A, list invitations 8 | // - As user B, accept invitation 9 | // - As user A, list collaborators (now includes user B) 10 | // - As user A, remove user B as collaborator from repository 11 | // - As user A, list collaborators (no longer includes user B) 12 | async function addAndRemoveRepositoryCollaborator(state) { 13 | // create a temporary repository 14 | const temporaryRepository = getTemporaryRepository({ 15 | request: state.request, 16 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 17 | org: "octokit-fixture-org", 18 | name: "add-and-remove-repository-collaborator", 19 | }); 20 | 21 | await temporaryRepository.create(); 22 | 23 | // https://developer.github.com/v3/repos/collaborators/#add-user-as-a-collaborator 24 | await state.request({ 25 | method: "put", 26 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/collaborators/octokit-fixture-user-b`, 27 | headers: { 28 | Accept: "application/vnd.github.v3+json", 29 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 30 | }, 31 | }); 32 | 33 | // https://developer.github.com/v3/repos/invitations/ 34 | const invitationsResponse = await state.request({ 35 | method: "get", 36 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/invitations`, 37 | headers: { 38 | Accept: "application/vnd.github.v3+json", 39 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 40 | }, 41 | }); 42 | 43 | // https://developer.github.com/v3/repos/invitations/#accept-a-repository-invitation 44 | await state.request({ 45 | method: "patch", 46 | url: `/user/repository_invitations/${invitationsResponse.data[0].id}`, 47 | headers: { 48 | Accept: "application/vnd.github.v3+json", 49 | Authorization: `token ${env.FIXTURES_USER_B_TOKEN_FULL_ACCESS}`, 50 | }, 51 | }); 52 | 53 | // wait for 1000ms as there seems to be a race condition on GitHub’s API 54 | await new Promise((resolve) => setTimeout(resolve, 1000)); 55 | 56 | // https://developer.github.com/v3/repos/collaborators/#list-collaborators 57 | await state.request({ 58 | method: "get", 59 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/collaborators`, 60 | headers: { 61 | Accept: "application/vnd.github.v3+json", 62 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 63 | }, 64 | }); 65 | 66 | // https://developer.github.com/v3/repos/collaborators/#remove-user-as-a-collaborator 67 | await state.request({ 68 | method: "delete", 69 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/collaborators/octokit-fixture-user-b`, 70 | headers: { 71 | Accept: "application/vnd.github.v3+json", 72 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 73 | }, 74 | }); 75 | 76 | // https://developer.github.com/v3/repos/collaborators/#list-collaborators 77 | await state.request({ 78 | method: "get", 79 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/collaborators`, 80 | headers: { 81 | Accept: "application/vnd.github.v3+json", 82 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 83 | }, 84 | }); 85 | 86 | await temporaryRepository.delete(); 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fixtures 2 | 3 | > Fixtures for all the octokittens 4 | 5 | ![Test](https://github.com/octokit/fixtures/workflows/Test/badge.svg) 6 | 7 | Records requests/responses against the [GitHub REST API](https://developer.github.com/v3/) 8 | and stores them as JSON fixtures. 9 | 10 | - [Usage](#usage) 11 | - [fixtures.mock(scenario)](#fixturesmockscenario) 12 | - [fixtures.get(scenario)](#fixturesgetscenario) 13 | - [fixtures.nock](#fixturesnock) 14 | - [How it works](HOW_IT_WORKS.md) 15 | - [Contributing](CONTRIBUTING.md) 16 | - [License](#license) 17 | 18 | ## Usage 19 | 20 | Currently requires node 8+ 21 | 22 | ### fixtures.mock(scenario) 23 | 24 | `fixtures.mock(scenario)` will intercept requests using [nock](https://www.npmjs.com/package/nock). 25 | `scenario` is a String in the form `/`. `host name` 26 | is any folder in [`scenarios/`](scenarios/). `scenario name` is any filename in 27 | the host name folders without the `.js` extension. 28 | 29 | ```js 30 | const https = require("https"); 31 | const fixtures = require("@octokit/fixtures"); 32 | 33 | fixtures.mock("api.github.com/get-repository"); 34 | https 35 | .request( 36 | { 37 | method: "GET", 38 | hostname: "api.github.com", 39 | path: "/repos/octokit-fixture-org/hello-world", 40 | headers: { 41 | accept: "application/vnd.github.v3+json", 42 | }, 43 | }, 44 | (response) => { 45 | console.log("headers:", response.headers); 46 | response.on("data", (data) => console.log(data.toString())); 47 | // logs response from fixture 48 | }, 49 | ) 50 | .end(); 51 | ``` 52 | 53 | For tests, you can check if all mocks have been satisfied for a given scenario 54 | 55 | ```js 56 | const mock = fixtures.mock("api.github.com/get-repository"); 57 | // send requests ... 58 | mock.done(); // will throw an error unless all mocked routes have been called 59 | mock.isDone(); // returns true / false 60 | mock.pending(); // returns array of pending mocks in the format [ ] 61 | ``` 62 | 63 | `mock.explain` can be used to amend an error thrown by nock if a request could 64 | not be matched 65 | 66 | ```js 67 | const mock = fixtures.mock("api.github.com/get-repository"); 68 | const github = new GitHub(); 69 | return github.repos 70 | .get({ owner: "octokit-fixture-org", repo: "hello-world" }) 71 | .catch(mock.explain); 72 | ``` 73 | 74 | Now instead of logging 75 | 76 | ``` 77 | Error: Nock: No match for request { 78 | "method": "get", 79 | "url": "https://api.github.com/orgs/octokit-fixture-org", 80 | "headers": { 81 | "host": "api.github.com", 82 | "content-length": "0", 83 | "user-agent": "NodeJS HTTP Client", 84 | "accept": "application/vnd.github.v3+json" 85 | } 86 | } 87 | ``` 88 | 89 | The log shows exactly what the difference between the sent request and the next 90 | pending mock is 91 | 92 | ```diff 93 | Request did not match mock: 94 | { 95 | headers: { 96 | - accept: "application/vnd.github.v3" 97 | + accept: "application/vnd.github.v3+json" 98 | } 99 | } 100 | ``` 101 | 102 | ### fixtures.get(scenario) 103 | 104 | `fixtures.get(scenario)` will return the JSON object which is used by [nock](https://www.npmjs.com/package/nock) 105 | to mock the API routes. You can use that method to convert the JSON to another 106 | format, for example. 107 | 108 | ### fixtures.nock 109 | 110 | `fixtures.nock` is the [nock](https://github.com/node-nock/nock) instance used 111 | internally by `@octokit/fixtures` for the http mocking. Use at your own peril :) 112 | 113 | ## License 114 | 115 | [MIT](LICENSE.md) 116 | -------------------------------------------------------------------------------- /scenarios/api.github.com/create-status/record.js: -------------------------------------------------------------------------------- 1 | export default createStatus; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function createStatus(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "create-status", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/repos/contents/#create-a-file 19 | // (this request gets ignored, we need an existing commit to set status on) 20 | const { 21 | data: { 22 | commit: { sha }, 23 | }, 24 | } = await state.request({ 25 | method: "put", 26 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/contents/README.md`, 27 | headers: { 28 | Accept: "application/vnd.github.v3+json", 29 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 30 | "X-Octokit-Fixture-Ignore": "true", 31 | }, 32 | data: { 33 | message: "initial commit", 34 | content: Buffer.from("# create-status").toString("base64"), 35 | }, 36 | }); 37 | 38 | // https://developer.github.com/v3/repos/statuses/#create-a-status 39 | await state.request({ 40 | method: "post", 41 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/statuses/${sha}`, 42 | headers: { 43 | Accept: "application/vnd.github.v3+json", 44 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 45 | }, 46 | data: { 47 | state: "failure", 48 | target_url: "https://example.com", 49 | description: "create-status failure test", 50 | context: "example/1", 51 | }, 52 | }); 53 | await state.request({ 54 | method: "post", 55 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/statuses/${sha}`, 56 | headers: { 57 | Accept: "application/vnd.github.v3+json", 58 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 59 | }, 60 | data: { 61 | state: "success", 62 | target_url: "https://example.com", 63 | description: "create-status success test", 64 | context: "example/2", 65 | }, 66 | }); 67 | 68 | // Wait for the created data to propagate 69 | await new Promise((resolve) => setTimeout(resolve, 3000)); 70 | 71 | // https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref 72 | await state.request({ 73 | method: "get", 74 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/commits/${sha}/statuses`, 75 | headers: { 76 | Accept: "application/vnd.github.v3+json", 77 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 78 | }, 79 | }); 80 | 81 | // https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref 82 | await state.request({ 83 | method: "get", 84 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/commits/${sha}/status`, 85 | headers: { 86 | Accept: "application/vnd.github.v3+json", 87 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 88 | }, 89 | }); 90 | } catch (_error) { 91 | error = _error; 92 | } 93 | 94 | await temporaryRepository.delete(); 95 | 96 | if (error) { 97 | return Promise.reject(error); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /scenarios/api.github.com/lock-issue/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "put", 5 | "path": "/repos/octokit-fixture-org/lock-issue/issues/1/lock", 6 | "body": "", 7 | "status": 204, 8 | "response": "", 9 | "reqheaders": { 10 | "accept": "application/vnd.github.v3+json", 11 | "authorization": "token 0000000000000000000000000000000000000001", 12 | "host": "api.github.com", 13 | "content-length": 0 14 | }, 15 | "responseIsBinary": false, 16 | "headers": { 17 | "access-control-allow-origin": "*", 18 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 19 | "connection": "close", 20 | "content-security-policy": "default-src 'none'", 21 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 22 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 23 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 24 | "x-accepted-oauth-scopes": "public_repo, repo", 25 | "x-content-type-options": "nosniff", 26 | "x-frame-options": "deny", 27 | "x-github-media-type": "github.v3; format=json", 28 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 29 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 30 | "x-ratelimit-limit": "5000", 31 | "x-ratelimit-remaining": "4999", 32 | "x-ratelimit-reset": "1507651200000", 33 | "x-ratelimit-resource": "core", 34 | "x-ratelimit-used": 1, 35 | "x-xss-protection": "0" 36 | } 37 | }, 38 | { 39 | "scope": "https://api.github.com:443", 40 | "method": "delete", 41 | "path": "/repos/octokit-fixture-org/lock-issue/issues/1/lock", 42 | "body": "", 43 | "status": 204, 44 | "response": "", 45 | "reqheaders": { 46 | "accept": "application/vnd.github.v3+json", 47 | "authorization": "token 0000000000000000000000000000000000000001", 48 | "host": "api.github.com" 49 | }, 50 | "responseIsBinary": false, 51 | "headers": { 52 | "access-control-allow-origin": "*", 53 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 54 | "connection": "close", 55 | "content-security-policy": "default-src 'none'", 56 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 57 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 58 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 59 | "x-accepted-oauth-scopes": "public_repo, repo", 60 | "x-content-type-options": "nosniff", 61 | "x-frame-options": "deny", 62 | "x-github-media-type": "github.v3; format=json", 63 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 64 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 65 | "x-ratelimit-limit": "5000", 66 | "x-ratelimit-remaining": "4999", 67 | "x-ratelimit-reset": "1507651200000", 68 | "x-ratelimit-resource": "core", 69 | "x-ratelimit-used": 1, 70 | "x-xss-protection": "0" 71 | } 72 | } 73 | ] 74 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-archive/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/repos/octokit-fixture-org/get-archive/tarball/main", 6 | "body": "", 7 | "status": 302, 8 | "response": "", 9 | "reqheaders": { 10 | "accept": "application/vnd.github.v3+json", 11 | "authorization": "token 0000000000000000000000000000000000000001", 12 | "accept-encoding": "gzip, compress, deflate, br", 13 | "host": "api.github.com" 14 | }, 15 | "responseIsBinary": false, 16 | "headers": { 17 | "access-control-allow-origin": "*", 18 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 19 | "cache-control": "public, must-revalidate, max-age=0", 20 | "connection": "close", 21 | "content-length": "0", 22 | "content-security-policy": "default-src 'none'", 23 | "content-type": "text/html;charset=utf-8", 24 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 25 | "expires": "Tue, 10 Oct 2017 16:00:00 GMT", 26 | "location": "https://codeload.github.com/octokit-fixture-org/get-archive/legacy.tar.gz/refs/heads/main", 27 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 28 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 29 | "x-content-type-options": "nosniff", 30 | "x-frame-options": "deny", 31 | "x-github-api-version-selected": "2022-11-28", 32 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 33 | "x-ratelimit-limit": "5000", 34 | "x-ratelimit-remaining": "4999", 35 | "x-ratelimit-reset": "1507651200000", 36 | "x-ratelimit-resource": "core", 37 | "x-ratelimit-used": 1, 38 | "x-xss-protection": "0" 39 | } 40 | }, 41 | { 42 | "scope": "https://codeload.github.com:443", 43 | "method": "get", 44 | "path": "/octokit-fixture-org/get-archive/legacy.tar.gz/refs/heads/main", 45 | "body": "", 46 | "status": 200, 47 | "response": "1f8b0800000000000003edd1b10ac2301006e03c4ac039fa2735b959b0a38b6f506aac412490a6e2e33b64d10ab55854c47ccb1172e472fcbe8efee8a2d8bb4bec82153e34a2b15154a13eb8b31548166c0200449ab3f454bfa6832ca4216948011c924813677acad0b1ba36568101c1fb38d4f7ecbebfdc8ff023f3df96abf5a69c9f762fcc0060cc72207fa9eff35752158a33bc61df077f9eff8cdfa4fdedcf645996651f730524cb59d6000a0000", 48 | "reqheaders": { 49 | "accept": "application/vnd.github.v3+json", 50 | "authorization": "token 0000000000000000000000000000000000000001", 51 | "accept-encoding": "gzip, compress, deflate, br", 52 | "host": "codeload.github.com" 53 | }, 54 | "responseIsBinary": true, 55 | "headers": { 56 | "access-control-allow-origin": "https://render.githubusercontent.com", 57 | "connection": "close", 58 | "content-disposition": "attachment; filename=octokit-fixture-org-get-archive-0000000.tar.gz", 59 | "content-length": 176, 60 | "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox", 61 | "content-type": "application/x-gzip", 62 | "cross-origin-resource-policy": "cross-origin", 63 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 64 | "etag": "\"00000000000000000000000000000000\"", 65 | "strict-transport-security": "max-age=31536000", 66 | "x-content-type-options": "nosniff", 67 | "x-frame-options": "deny", 68 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 69 | "x-xss-protection": "1; mode=block" 70 | } 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /scenarios/api.github.com/markdown/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "post", 5 | "path": "/markdown", 6 | "body": { 7 | "text": "### Hello\n\nb597b5d", 8 | "context": "octokit-fixture-org/hello-world", 9 | "mode": "gfm" 10 | }, 11 | "status": 200, 12 | "response": "

Hello

\n

b597b5d

", 13 | "reqheaders": { 14 | "accept": "text/html", 15 | "content-type": "application/json; charset=utf-8", 16 | "authorization": "token 0000000000000000000000000000000000000001", 17 | "content-length": 88, 18 | "host": "api.github.com" 19 | }, 20 | "responseIsBinary": false, 21 | "headers": { 22 | "access-control-allow-origin": "*", 23 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 24 | "connection": "close", 25 | "content-length": "352", 26 | "content-security-policy": "default-src 'none'", 27 | "content-type": "text/html;charset=utf-8", 28 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 29 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 30 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 31 | "x-commonmarker-version": "0.23.5", 32 | "x-content-type-options": "nosniff", 33 | "x-frame-options": "deny", 34 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 35 | "x-ratelimit-limit": "5000", 36 | "x-ratelimit-remaining": "4999", 37 | "x-ratelimit-reset": "1507651200000", 38 | "x-ratelimit-resource": "core", 39 | "x-ratelimit-used": 1, 40 | "x-xss-protection": "0" 41 | } 42 | }, 43 | { 44 | "scope": "https://api.github.com:443", 45 | "method": "post", 46 | "path": "/markdown/raw", 47 | "body": "### Hello\n\nb597b5d", 48 | "status": 200, 49 | "response": "

\nHello

\n

b597b5d

\n", 50 | "reqheaders": { 51 | "accept": "text/html", 52 | "content-type": "text/plain; charset=utf-8", 53 | "authorization": "token 0000000000000000000000000000000000000001", 54 | "content-length": 18, 55 | "host": "api.github.com" 56 | }, 57 | "responseIsBinary": false, 58 | "headers": { 59 | "access-control-allow-origin": "*", 60 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 61 | "connection": "close", 62 | "content-length": "171", 63 | "content-security-policy": "default-src 'none'", 64 | "content-type": "text/html;charset=utf-8", 65 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 66 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 67 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 68 | "x-commonmarker-version": "0.23.5", 69 | "x-content-type-options": "nosniff", 70 | "x-frame-options": "deny", 71 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 72 | "x-ratelimit-limit": "5000", 73 | "x-ratelimit-remaining": "4999", 74 | "x-ratelimit-reset": "1507651200000", 75 | "x-ratelimit-resource": "core", 76 | "x-ratelimit-used": 1, 77 | "x-xss-protection": "0" 78 | } 79 | } 80 | ] 81 | -------------------------------------------------------------------------------- /scenarios/api.github.com/project-cards/test.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import fixtures from "../../../index.js"; 4 | 5 | test("Labels", async () => { 6 | const mock = fixtures.mock("api.github.com/project-cards"); 7 | 8 | // https://developer.github.com/v3/projects/cards/#create-a-project-card 9 | await axios({ 10 | method: "post", 11 | url: "https://api.github.com/projects/columns/1000/cards", 12 | headers: { 13 | Accept: "application/vnd.github.v3+json", 14 | Authorization: "token 0000000000000000000000000000000000000001", 15 | "Content-Type": "application/json; charset=utf-8", 16 | }, 17 | data: { 18 | note: "Example card 1", 19 | }, 20 | }); 21 | await axios({ 22 | method: "post", 23 | url: "https://api.github.com/projects/columns/1000/cards", 24 | headers: { 25 | Accept: "application/vnd.github.v3+json", 26 | Authorization: "token 0000000000000000000000000000000000000001", 27 | "Content-Type": "application/json; charset=utf-8", 28 | }, 29 | data: { 30 | note: "Example card 2", 31 | }, 32 | }); 33 | 34 | // https://developer.github.com/v3/projects/cards/#list-project-cards 35 | await axios({ 36 | method: "get", 37 | url: "https://api.github.com/projects/columns/1000/cards", 38 | headers: { 39 | Accept: "application/vnd.github.v3+json", 40 | Authorization: "token 0000000000000000000000000000000000000001", 41 | }, 42 | }); 43 | 44 | // https://developer.github.com/v3/projects/cards/#get-a-project-card 45 | await axios({ 46 | method: "get", 47 | url: "https://api.github.com/projects/columns/cards/1000", 48 | headers: { 49 | Accept: "application/vnd.github.v3+json", 50 | Authorization: "token 0000000000000000000000000000000000000001", 51 | }, 52 | }); 53 | 54 | // https://developer.github.com/v3/projects/cards/#update-a-project-card 55 | await axios({ 56 | method: "patch", 57 | url: "https://api.github.com/projects/columns/cards/1000", 58 | headers: { 59 | Accept: "application/vnd.github.v3+json", 60 | Authorization: "token 0000000000000000000000000000000000000001", 61 | "Content-Type": "application/json; charset=utf-8", 62 | }, 63 | data: { 64 | note: "Example card 1 updated", 65 | }, 66 | }); 67 | 68 | // https://developer.github.com/v3/projects/cards/#move-a-project-card 69 | // move 1st card to 2nd column 70 | await axios({ 71 | method: "post", 72 | url: "https://api.github.com/projects/columns/cards/1000/moves", 73 | headers: { 74 | Accept: "application/vnd.github.v3+json", 75 | Authorization: "token 0000000000000000000000000000000000000001", 76 | "Content-Type": "application/json; charset=utf-8", 77 | }, 78 | data: { 79 | position: "top", 80 | column_id: 1001, 81 | }, 82 | }); 83 | 84 | // move 2nd card to bottom of 2nd column 85 | await axios({ 86 | method: "post", 87 | url: "https://api.github.com/projects/columns/cards/1001/moves", 88 | headers: { 89 | Accept: "application/vnd.github.v3+json", 90 | Authorization: "token 0000000000000000000000000000000000000001", 91 | "Content-Type": "application/json; charset=utf-8", 92 | }, 93 | data: { 94 | position: "bottom", 95 | column_id: 1001, 96 | }, 97 | }); 98 | 99 | // move 1st card below 2nd card 100 | await axios({ 101 | method: "post", 102 | url: "https://api.github.com/projects/columns/cards/1000/moves", 103 | headers: { 104 | Accept: "application/vnd.github.v3+json", 105 | Authorization: "token 0000000000000000000000000000000000000001", 106 | "Content-Type": "application/json; charset=utf-8", 107 | }, 108 | data: { 109 | position: "after:1001", 110 | }, 111 | }); 112 | 113 | // https://developer.github.com/v3/projects/cards/#delete-a-project-card 114 | await axios({ 115 | method: "delete", 116 | url: "https://api.github.com/projects/columns/cards/1000", 117 | headers: { 118 | Accept: "application/vnd.github.v3+json", 119 | Authorization: "token 0000000000000000000000000000000000000001", 120 | }, 121 | }); 122 | 123 | expect(mock.done.bind(mock)).not.toThrow(); 124 | }); 125 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-organization/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/orgs/octokit-fixture-org", 6 | "body": "", 7 | "status": 200, 8 | "response": { 9 | "login": "octokit-fixture-org", 10 | "id": 1000, 11 | "node_id": "MDA6RW50aXR5MQ==", 12 | "url": "https://api.github.com/orgs/octokit-fixture-org", 13 | "repos_url": "https://api.github.com/orgs/octokit-fixture-org/repos", 14 | "events_url": "https://api.github.com/orgs/octokit-fixture-org/events", 15 | "hooks_url": "https://api.github.com/orgs/octokit-fixture-org/hooks", 16 | "issues_url": "https://api.github.com/orgs/octokit-fixture-org/issues", 17 | "members_url": "https://api.github.com/orgs/octokit-fixture-org/members{/member}", 18 | "public_members_url": "https://api.github.com/orgs/octokit-fixture-org/public_members{/member}", 19 | "avatar_url": "https://avatars.githubusercontent.com/u/1000?v=4", 20 | "description": null, 21 | "is_verified": false, 22 | "has_organization_projects": true, 23 | "has_repository_projects": true, 24 | "public_repos": 42, 25 | "public_gists": 0, 26 | "followers": 0, 27 | "following": 0, 28 | "html_url": "https://github.com/octokit-fixture-org", 29 | "created_at": "2017-10-10T16:00:00Z", 30 | "updated_at": "2017-10-10T16:00:00Z", 31 | "type": "Organization", 32 | "total_private_repos": 0, 33 | "owned_private_repos": 0, 34 | "private_gists": 0, 35 | "disk_usage": 1, 36 | "collaborators": 0, 37 | "billing_email": "octokit-fixture-user-a@kytrinyx.com", 38 | "default_repository_permission": "read", 39 | "members_can_create_repositories": true, 40 | "two_factor_requirement_enabled": false, 41 | "members_allowed_repository_creation_type": "all", 42 | "members_can_create_public_repositories": true, 43 | "members_can_create_private_repositories": true, 44 | "members_can_create_internal_repositories": false, 45 | "members_can_create_pages": true, 46 | "members_can_fork_private_repositories": false, 47 | "members_can_create_public_pages": true, 48 | "members_can_create_private_pages": true, 49 | "web_commit_signoff_required": false, 50 | "plan": { 51 | "name": "team", 52 | "space": 976562499, 53 | "private_repos": 999999, 54 | "filled_seats": 1, 55 | "seats": 5 56 | } 57 | }, 58 | "reqheaders": { 59 | "accept": "application/vnd.github.v3+json", 60 | "authorization": "token 0000000000000000000000000000000000000001", 61 | "host": "api.github.com" 62 | }, 63 | "responseIsBinary": false, 64 | "headers": { 65 | "access-control-allow-origin": "*", 66 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 67 | "cache-control": "private, max-age=60, s-maxage=60", 68 | "connection": "close", 69 | "content-length": "1699", 70 | "content-security-policy": "default-src 'none'", 71 | "content-type": "application/json; charset=utf-8", 72 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 73 | "etag": "\"00000000000000000000000000000000\"", 74 | "last-modified": "Tue, 10 Oct 2017 16:00:00 GMT", 75 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 76 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 77 | "x-accepted-oauth-scopes": "admin:org, read:org, repo, user, write:org", 78 | "x-content-type-options": "nosniff", 79 | "x-frame-options": "deny", 80 | "x-github-media-type": "github.v3; format=json", 81 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 82 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 83 | "x-ratelimit-limit": "5000", 84 | "x-ratelimit-remaining": "4999", 85 | "x-ratelimit-reset": "1507651200000", 86 | "x-ratelimit-resource": "core", 87 | "x-ratelimit-used": 1, 88 | "x-xss-protection": "0" 89 | } 90 | } 91 | ] 92 | -------------------------------------------------------------------------------- /scenarios/api.github.com/create-file/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "put", 5 | "path": "/repos/octokit-fixture-org/create-file/contents/test.txt", 6 | "body": { "message": "create test.txt", "content": "VGVzdCBjb250ZW50" }, 7 | "status": 201, 8 | "response": { 9 | "content": { 10 | "name": "test.txt", 11 | "path": "test.txt", 12 | "sha": "3f3f005b29247e51a4f4d6b8ce07b67646cd6074", 13 | "size": 12, 14 | "url": "https://api.github.com/repos/octokit-fixture-org/create-file/contents/test.txt?ref=main", 15 | "html_url": "https://github.com/octokit-fixture-org/create-file/blob/main/test.txt", 16 | "git_url": "https://api.github.com/repos/octokit-fixture-org/create-file/git/blobs/3f3f005b29247e51a4f4d6b8ce07b67646cd6074", 17 | "download_url": "https://raw.githubusercontent.com/octokit-fixture-org/create-file/main/test.txt", 18 | "type": "file", 19 | "_links": { 20 | "self": "https://api.github.com/repos/octokit-fixture-org/create-file/contents/test.txt?ref=main", 21 | "git": "https://api.github.com/repos/octokit-fixture-org/create-file/git/blobs/3f3f005b29247e51a4f4d6b8ce07b67646cd6074", 22 | "html": "https://github.com/octokit-fixture-org/create-file/blob/main/test.txt" 23 | } 24 | }, 25 | "commit": { 26 | "sha": "0000000000000000000000000000000000000002", 27 | "node_id": "MDA6RW50aXR5MQ==", 28 | "url": "https://api.github.com/repos/octokit-fixture-org/create-file/git/commits/0000000000000000000000000000000000000002", 29 | "html_url": "https://github.com/octokit-fixture-org/create-file/commit/0000000000000000000000000000000000000002", 30 | "author": { 31 | "name": "octokit-fixture-user-a", 32 | "email": "31898046+octokit-fixture-user-a@users.noreply.github.com", 33 | "date": "2017-10-10T16:00:00Z" 34 | }, 35 | "committer": { 36 | "name": "octokit-fixture-user-a", 37 | "email": "31898046+octokit-fixture-user-a@users.noreply.github.com", 38 | "date": "2017-10-10T16:00:00Z" 39 | }, 40 | "tree": { 41 | "sha": "0000000000000000000000000000000000000001", 42 | "url": "https://api.github.com/repos/octokit-fixture-org/create-file/git/trees/0000000000000000000000000000000000000001" 43 | }, 44 | "message": "create test.txt", 45 | "parents": [], 46 | "verification": { 47 | "verified": false, 48 | "reason": "unsigned", 49 | "signature": null, 50 | "payload": null 51 | } 52 | } 53 | }, 54 | "reqheaders": { 55 | "accept": "application/vnd.github.v3+json", 56 | "content-type": "application/json; charset=utf-8", 57 | "authorization": "token 0000000000000000000000000000000000000001", 58 | "content-length": 58, 59 | "host": "api.github.com" 60 | }, 61 | "responseIsBinary": false, 62 | "headers": { 63 | "access-control-allow-origin": "*", 64 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 65 | "cache-control": "private, max-age=60, s-maxage=60", 66 | "connection": "close", 67 | "content-length": "1740", 68 | "content-security-policy": "default-src 'none'", 69 | "content-type": "application/json; charset=utf-8", 70 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 71 | "etag": "\"00000000000000000000000000000000\"", 72 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 73 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 74 | "x-accepted-oauth-scopes": "", 75 | "x-content-type-options": "nosniff", 76 | "x-frame-options": "deny", 77 | "x-github-media-type": "github.v3; format=json", 78 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 79 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 80 | "x-ratelimit-limit": "5000", 81 | "x-ratelimit-remaining": "4999", 82 | "x-ratelimit-reset": "1507651200000", 83 | "x-ratelimit-resource": "core", 84 | "x-ratelimit-used": 1, 85 | "x-xss-protection": "0" 86 | } 87 | } 88 | ] 89 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-root/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/", 6 | "body": "", 7 | "status": 200, 8 | "response": { 9 | "current_user_url": "https://api.github.com/user", 10 | "current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}", 11 | "authorizations_url": "https://api.github.com/authorizations", 12 | "code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}", 13 | "commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}", 14 | "emails_url": "https://api.github.com/user/emails", 15 | "emojis_url": "https://api.github.com/emojis", 16 | "events_url": "https://api.github.com/events", 17 | "feeds_url": "https://api.github.com/feeds", 18 | "followers_url": "https://api.github.com/user/followers", 19 | "following_url": "https://api.github.com/user/following{/target}", 20 | "gists_url": "https://api.github.com/gists{/gist_id}", 21 | "hub_url": "https://api.github.com/hub", 22 | "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}", 23 | "issues_url": "https://api.github.com/issues", 24 | "keys_url": "https://api.github.com/user/keys", 25 | "label_search_url": "https://api.github.com/search/labels?q={query}&repository_id={repository_id}{&page,per_page}", 26 | "notifications_url": "https://api.github.com/notifications", 27 | "organization_url": "https://api.github.com/orgs/{org}", 28 | "organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}", 29 | "organization_teams_url": "https://api.github.com/orgs/{org}/teams", 30 | "public_gists_url": "https://api.github.com/gists/public", 31 | "rate_limit_url": "https://api.github.com/rate_limit", 32 | "repository_url": "https://api.github.com/repos/{owner}/{repo}", 33 | "repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}", 34 | "current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}", 35 | "starred_url": "https://api.github.com/user/starred{/owner}{/repo}", 36 | "starred_gists_url": "https://api.github.com/gists/starred", 37 | "topic_search_url": "https://api.github.com/search/topics?q={query}{&page,per_page}", 38 | "user_url": "https://api.github.com/users/{user}", 39 | "user_organizations_url": "https://api.github.com/user/orgs", 40 | "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}", 41 | "user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}" 42 | }, 43 | "reqheaders": { 44 | "accept": "application/vnd.github.v3+json", 45 | "authorization": "token 0000000000000000000000000000000000000001", 46 | "host": "api.github.com" 47 | }, 48 | "responseIsBinary": false, 49 | "headers": { 50 | "access-control-allow-origin": "*", 51 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 52 | "cache-control": "private, max-age=60, s-maxage=60", 53 | "connection": "close", 54 | "content-length": "2262", 55 | "content-security-policy": "default-src 'none'", 56 | "content-type": "application/json; charset=utf-8", 57 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 58 | "etag": "\"00000000000000000000000000000000\"", 59 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 60 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 61 | "x-accepted-oauth-scopes": "", 62 | "x-content-type-options": "nosniff", 63 | "x-frame-options": "deny", 64 | "x-github-media-type": "github.v3; format=json", 65 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 66 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 67 | "x-ratelimit-limit": "5000", 68 | "x-ratelimit-remaining": "4999", 69 | "x-ratelimit-reset": "1507651200000", 70 | "x-ratelimit-resource": "core", 71 | "x-ratelimit-used": 1, 72 | "x-xss-protection": "0" 73 | } 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /scenarios/api.github.com/git-refs/record.js: -------------------------------------------------------------------------------- 1 | export default gitRefs; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function gitRefs(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "git-refs", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/repos/contents/#create-a-file 19 | // (these requests get ignored, we need two commits to test our references) 20 | const { 21 | data: { 22 | commit: { sha: sha1 }, 23 | }, 24 | } = await state.request({ 25 | method: "put", 26 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/contents/1.md`, 27 | headers: { 28 | Accept: "application/vnd.github.v3+json", 29 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 30 | "X-Octokit-Fixture-Ignore": "true", 31 | }, 32 | data: { 33 | message: "initial commit", 34 | content: Buffer.from( 35 | "# git-refs\ncreated with initial commit", 36 | ).toString("base64"), 37 | }, 38 | }); 39 | const { 40 | data: { 41 | commit: { sha: sha2 }, 42 | }, 43 | } = await state.request({ 44 | method: "put", 45 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/contents/2.md`, 46 | headers: { 47 | Accept: "application/vnd.github.v3+json", 48 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 49 | "X-Octokit-Fixture-Ignore": "true", 50 | }, 51 | data: { 52 | message: "2nd commit", 53 | content: Buffer.from("# git-refs\ncreated with 2nd commit").toString( 54 | "base64", 55 | ), 56 | }, 57 | }); 58 | 59 | // https://developer.github.com/v3/git/refs/#get-all-references 60 | // returns a single reference for the master branch, pointing to sha of 2nd commit 61 | await state.request({ 62 | method: "get", 63 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/git/refs/`, 64 | headers: { 65 | Accept: "application/vnd.github.v3+json", 66 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 67 | }, 68 | }); 69 | 70 | // https://developer.github.com/v3/git/refs/#create-a-reference 71 | // Create a new branch "test" pointing to sha of initial commit 72 | await state.request({ 73 | method: "post", 74 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/git/refs`, 75 | headers: { 76 | Accept: "application/vnd.github.v3+json", 77 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 78 | }, 79 | data: { 80 | ref: "refs/heads/test", 81 | sha: sha1, 82 | }, 83 | }); 84 | 85 | // https://developer.github.com/v3/git/refs/#update-a-reference 86 | // update test branch to point to sha of 2nd commit instead 87 | await state.request({ 88 | method: "patch", 89 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/git/refs/heads/test`, 90 | headers: { 91 | Accept: "application/vnd.github.v3+json", 92 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 93 | }, 94 | data: { 95 | sha: sha2, 96 | }, 97 | }); 98 | 99 | // https://developer.github.com/v3/git/refs/#get-all-references 100 | // Now returns both branches: master & test 101 | await state.request({ 102 | method: "get", 103 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/git/refs/`, 104 | headers: { 105 | Accept: "application/vnd.github.v3+json", 106 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 107 | }, 108 | }); 109 | 110 | // https://developer.github.com/v3/git/refs/#delete-a-reference 111 | // Delete test branch 112 | await state.request({ 113 | method: "delete", 114 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/git/refs/heads/test`, 115 | headers: { 116 | Accept: "application/vnd.github.v3+json", 117 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 118 | }, 119 | }); 120 | } catch (_error) { 121 | error = _error; 122 | } 123 | 124 | await temporaryRepository.delete(); 125 | 126 | if (error) { 127 | return Promise.reject(error); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /scenarios/api.github.com/markdown/raw-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "post", 5 | "path": "/markdown", 6 | "body": { 7 | "text": "### Hello\n\nb597b5d", 8 | "context": "octokit-fixture-org/hello-world", 9 | "mode": "gfm" 10 | }, 11 | "status": 200, 12 | "response": "

Hello

\n

b597b5d

", 13 | "rawHeaders": [ 14 | "Server", 15 | "GitHub.com", 16 | "Date", 17 | "Tue, 19 Jul 2022 04:38:33 GMT", 18 | "Content-Type", 19 | "text/html;charset=utf-8", 20 | "Content-Length", 21 | "352", 22 | "x-commonmarker-version", 23 | "0.23.5", 24 | "X-RateLimit-Limit", 25 | "5000", 26 | "X-RateLimit-Remaining", 27 | "4938", 28 | "X-RateLimit-Reset", 29 | "1658208999", 30 | "X-RateLimit-Used", 31 | "62", 32 | "X-RateLimit-Resource", 33 | "core", 34 | "Access-Control-Expose-Headers", 35 | "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 36 | "Access-Control-Allow-Origin", 37 | "*", 38 | "Strict-Transport-Security", 39 | "max-age=31536000; includeSubdomains; preload", 40 | "X-Frame-Options", 41 | "deny", 42 | "X-Content-Type-Options", 43 | "nosniff", 44 | "X-XSS-Protection", 45 | "0", 46 | "Referrer-Policy", 47 | "origin-when-cross-origin, strict-origin-when-cross-origin", 48 | "Content-Security-Policy", 49 | "default-src 'none'", 50 | "Vary", 51 | "Accept-Encoding, Accept, X-Requested-With", 52 | "X-GitHub-Request-Id", 53 | "0684:69AD:85D195:18092E1:62D63549", 54 | "connection", 55 | "close" 56 | ], 57 | "reqheaders": { 58 | "accept": "text/html", 59 | "content-type": "application/json", 60 | "authorization": "token 0000000000000000000000000000000000000001", 61 | "content-length": 88, 62 | "host": "api.github.com" 63 | }, 64 | "responseIsBinary": false 65 | }, 66 | { 67 | "scope": "https://api.github.com:443", 68 | "method": "post", 69 | "path": "/markdown/raw", 70 | "body": "### Hello\n\nb597b5d", 71 | "status": 200, 72 | "response": "

\nHello

\n

b597b5d

\n", 73 | "rawHeaders": [ 74 | "Server", 75 | "GitHub.com", 76 | "Date", 77 | "Tue, 19 Jul 2022 04:38:36 GMT", 78 | "Content-Type", 79 | "text/html;charset=utf-8", 80 | "Content-Length", 81 | "171", 82 | "x-commonmarker-version", 83 | "0.23.5", 84 | "X-RateLimit-Limit", 85 | "5000", 86 | "X-RateLimit-Remaining", 87 | "4937", 88 | "X-RateLimit-Reset", 89 | "1658208999", 90 | "X-RateLimit-Used", 91 | "63", 92 | "X-RateLimit-Resource", 93 | "core", 94 | "Access-Control-Expose-Headers", 95 | "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 96 | "Access-Control-Allow-Origin", 97 | "*", 98 | "Strict-Transport-Security", 99 | "max-age=31536000; includeSubdomains; preload", 100 | "X-Frame-Options", 101 | "deny", 102 | "X-Content-Type-Options", 103 | "nosniff", 104 | "X-XSS-Protection", 105 | "0", 106 | "Referrer-Policy", 107 | "origin-when-cross-origin, strict-origin-when-cross-origin", 108 | "Content-Security-Policy", 109 | "default-src 'none'", 110 | "Vary", 111 | "Accept-Encoding, Accept, X-Requested-With", 112 | "X-GitHub-Request-Id", 113 | "0682:0EDB:1118E3:2054E0:62D6354C", 114 | "connection", 115 | "close" 116 | ], 117 | "reqheaders": { 118 | "accept": "text/html", 119 | "content-type": "text/plain; charset=utf-8", 120 | "authorization": "token 0000000000000000000000000000000000000001", 121 | "content-length": 18, 122 | "host": "api.github.com" 123 | }, 124 | "responseIsBinary": false 125 | } 126 | ] 127 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-organization/raw-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/orgs/octokit-fixture-org", 6 | "body": "", 7 | "status": 200, 8 | "response": { 9 | "login": "octokit-fixture-org", 10 | "id": 31898100, 11 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjMxODk4MTAw", 12 | "url": "https://api.github.com/orgs/octokit-fixture-org", 13 | "repos_url": "https://api.github.com/orgs/octokit-fixture-org/repos", 14 | "events_url": "https://api.github.com/orgs/octokit-fixture-org/events", 15 | "hooks_url": "https://api.github.com/orgs/octokit-fixture-org/hooks", 16 | "issues_url": "https://api.github.com/orgs/octokit-fixture-org/issues", 17 | "members_url": "https://api.github.com/orgs/octokit-fixture-org/members{/member}", 18 | "public_members_url": "https://api.github.com/orgs/octokit-fixture-org/public_members{/member}", 19 | "avatar_url": "https://avatars.githubusercontent.com/u/31898100?v=4", 20 | "description": null, 21 | "is_verified": false, 22 | "has_organization_projects": true, 23 | "has_repository_projects": true, 24 | "public_repos": 316, 25 | "public_gists": 0, 26 | "followers": 0, 27 | "following": 0, 28 | "html_url": "https://github.com/octokit-fixture-org", 29 | "created_at": "2017-09-12T16:55:36Z", 30 | "updated_at": "2022-03-14T15:34:56Z", 31 | "type": "Organization", 32 | "total_private_repos": 0, 33 | "owned_private_repos": 0, 34 | "private_gists": 0, 35 | "disk_usage": 1, 36 | "collaborators": 0, 37 | "billing_email": "octokit-fixture-user-a@kytrinyx.com", 38 | "default_repository_permission": "read", 39 | "members_can_create_repositories": true, 40 | "two_factor_requirement_enabled": false, 41 | "members_allowed_repository_creation_type": "all", 42 | "members_can_create_public_repositories": true, 43 | "members_can_create_private_repositories": true, 44 | "members_can_create_internal_repositories": false, 45 | "members_can_create_pages": true, 46 | "members_can_fork_private_repositories": false, 47 | "members_can_create_public_pages": true, 48 | "members_can_create_private_pages": true, 49 | "web_commit_signoff_required": false, 50 | "plan": { 51 | "name": "team", 52 | "space": 976562499, 53 | "private_repos": 999999, 54 | "filled_seats": 1, 55 | "seats": 5 56 | } 57 | }, 58 | "rawHeaders": [ 59 | "Server", 60 | "GitHub.com", 61 | "Date", 62 | "Tue, 19 Jul 2022 04:37:49 GMT", 63 | "Content-Type", 64 | "application/json; charset=utf-8", 65 | "Content-Length", 66 | "1724", 67 | "Cache-Control", 68 | "private, max-age=60, s-maxage=60", 69 | "Vary", 70 | "Accept, Authorization, Cookie, X-GitHub-OTP", 71 | "ETag", 72 | "\"ee932ded00b8a5cb7e4721f4c6e4e0ab21a5e60c192f3f71a141f8b50e20f8ed\"", 73 | "Last-Modified", 74 | "Mon, 14 Mar 2022 15:34:56 GMT", 75 | "X-OAuth-Scopes", 76 | "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 77 | "X-Accepted-OAuth-Scopes", 78 | "admin:org, read:org, repo, user, write:org", 79 | "X-GitHub-Media-Type", 80 | "github.v3; format=json", 81 | "X-RateLimit-Limit", 82 | "5000", 83 | "X-RateLimit-Remaining", 84 | "4963", 85 | "X-RateLimit-Reset", 86 | "1658208999", 87 | "X-RateLimit-Used", 88 | "37", 89 | "X-RateLimit-Resource", 90 | "core", 91 | "Access-Control-Expose-Headers", 92 | "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 93 | "Access-Control-Allow-Origin", 94 | "*", 95 | "Strict-Transport-Security", 96 | "max-age=31536000; includeSubdomains; preload", 97 | "X-Frame-Options", 98 | "deny", 99 | "X-Content-Type-Options", 100 | "nosniff", 101 | "X-XSS-Protection", 102 | "0", 103 | "Referrer-Policy", 104 | "origin-when-cross-origin, strict-origin-when-cross-origin", 105 | "Content-Security-Policy", 106 | "default-src 'none'", 107 | "Vary", 108 | "Accept-Encoding, Accept, X-Requested-With", 109 | "X-GitHub-Request-Id", 110 | "0686:6B69:1973B39:3A65994:62D6351D", 111 | "connection", 112 | "close" 113 | ], 114 | "reqheaders": { 115 | "accept": "application/vnd.github.v3+json", 116 | "authorization": "token 0000000000000000000000000000000000000001", 117 | "host": "api.github.com" 118 | }, 119 | "responseIsBinary": false 120 | } 121 | ] 122 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-root/raw-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/", 6 | "body": "", 7 | "status": 200, 8 | "response": { 9 | "current_user_url": "https://api.github.com/user", 10 | "current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}", 11 | "authorizations_url": "https://api.github.com/authorizations", 12 | "code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}", 13 | "commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}", 14 | "emails_url": "https://api.github.com/user/emails", 15 | "emojis_url": "https://api.github.com/emojis", 16 | "events_url": "https://api.github.com/events", 17 | "feeds_url": "https://api.github.com/feeds", 18 | "followers_url": "https://api.github.com/user/followers", 19 | "following_url": "https://api.github.com/user/following{/target}", 20 | "gists_url": "https://api.github.com/gists{/gist_id}", 21 | "hub_url": "https://api.github.com/hub", 22 | "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}", 23 | "issues_url": "https://api.github.com/issues", 24 | "keys_url": "https://api.github.com/user/keys", 25 | "label_search_url": "https://api.github.com/search/labels?q={query}&repository_id={repository_id}{&page,per_page}", 26 | "notifications_url": "https://api.github.com/notifications", 27 | "organization_url": "https://api.github.com/orgs/{org}", 28 | "organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}", 29 | "organization_teams_url": "https://api.github.com/orgs/{org}/teams", 30 | "public_gists_url": "https://api.github.com/gists/public", 31 | "rate_limit_url": "https://api.github.com/rate_limit", 32 | "repository_url": "https://api.github.com/repos/{owner}/{repo}", 33 | "repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}", 34 | "current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}", 35 | "starred_url": "https://api.github.com/user/starred{/owner}{/repo}", 36 | "starred_gists_url": "https://api.github.com/gists/starred", 37 | "topic_search_url": "https://api.github.com/search/topics?q={query}{&page,per_page}", 38 | "user_url": "https://api.github.com/users/{user}", 39 | "user_organizations_url": "https://api.github.com/user/orgs", 40 | "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}", 41 | "user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}" 42 | }, 43 | "rawHeaders": [ 44 | "Server", 45 | "GitHub.com", 46 | "Date", 47 | "Tue, 19 Jul 2022 04:37:49 GMT", 48 | "Content-Type", 49 | "application/json; charset=utf-8", 50 | "Content-Length", 51 | "2262", 52 | "Cache-Control", 53 | "private, max-age=60, s-maxage=60", 54 | "Vary", 55 | "Accept, Authorization, Cookie, X-GitHub-OTP", 56 | "ETag", 57 | "\"0050bc445d4d6ef228679cbd631e53c29050356fb2e27cfefa29b9d78c5dc15b\"", 58 | "X-OAuth-Scopes", 59 | "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 60 | "X-Accepted-OAuth-Scopes", 61 | "", 62 | "X-GitHub-Media-Type", 63 | "github.v3; format=json", 64 | "X-RateLimit-Limit", 65 | "5000", 66 | "X-RateLimit-Remaining", 67 | "4961", 68 | "X-RateLimit-Reset", 69 | "1658208999", 70 | "X-RateLimit-Used", 71 | "39", 72 | "X-RateLimit-Resource", 73 | "core", 74 | "Access-Control-Expose-Headers", 75 | "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 76 | "Access-Control-Allow-Origin", 77 | "*", 78 | "Strict-Transport-Security", 79 | "max-age=31536000; includeSubdomains; preload", 80 | "X-Frame-Options", 81 | "deny", 82 | "X-Content-Type-Options", 83 | "nosniff", 84 | "X-XSS-Protection", 85 | "0", 86 | "Referrer-Policy", 87 | "origin-when-cross-origin, strict-origin-when-cross-origin", 88 | "Content-Security-Policy", 89 | "default-src 'none'", 90 | "Vary", 91 | "Accept-Encoding, Accept, X-Requested-With", 92 | "X-GitHub-Request-Id", 93 | "0688:49B0:2044BC3:6263636:62D6351D", 94 | "connection", 95 | "close" 96 | ], 97 | "reqheaders": { 98 | "accept": "application/vnd.github.v3+json", 99 | "authorization": "token 0000000000000000000000000000000000000001", 100 | "host": "api.github.com" 101 | }, 102 | "responseIsBinary": false 103 | } 104 | ] 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | * [Requirements & local setup](#requirements--local-setup) 4 | * [Test users / organization / tokens](#test-users--organization--tokens) 5 | * [Record](#record) 6 | * [Tests](#tests) 7 | * [Coverage](#coverage) 8 | 9 | Thanks for wanting to contribute to the Octokit Fixtures, your help is more than 10 | welcome. If you have a question about contributing, please open an issue! 11 | 12 | A general overview of how Octokit Fixtures work, have a look at [HOW_IT_WORKS.md](HOW_IT_WORKS.md). 13 | 14 | Please abide by our [Code of Conduct](CODE_OF_CONDUCT.md). 15 | 16 | ## Requirements & local setup 17 | 18 | Octokit Fixtures require Node 8 in order to run its tests. 19 | 20 | The basic setup is 21 | 22 | ``` 23 | git clone https://github.com/octokit/fixtures.git octokit-fixtures 24 | cd octokit-fixtures 25 | npm install 26 | npm test 27 | ``` 28 | 29 | ## Test users / organization / tokens 30 | 31 | GitHub created test user accounts and an organization for the octokit fixtures. 32 | In order to run the `bin/record.js` script, you will need to configure the 33 | environment variables listed below. If you need the tokens in order to contribute, 34 | please let us know and we will provide them to you. 35 | 36 | 37 | 38 | 41 | 45 | 46 | 47 | 50 | 54 | 55 | 56 | 59 | 63 | 64 |
39 | 40 | 42 | octokit-fixture-user-a (user)
43 | Main user, has unlimited private repositories 44 |
48 | 49 | 51 | octokit-fixture-user-b (user)
52 | Secondary user, private repositories only 53 |
57 | 58 | 60 | octokit-fixture-org (org)
61 | Main organization, unlimited private repositories, unlimited seats 62 |
65 | 66 | The following access tokens need to be configured as environment variables. You 67 | can create a `.env` file to configure them locally, it is parsed using [dotenv](https://www.npmjs.com/package/dotenv). 68 | 69 | 70 | 71 | 72 | 75 | 78 | 81 | 84 | 85 | 86 | 87 | 90 | 93 | 96 | 99 | 100 | 101 | 104 | 107 | 110 | 113 | 114 |
73 | # 74 | 76 | Environment variable 77 | 79 | User 80 | 82 | Description 83 |
88 | 1 89 | 91 | FIXTURES_USER_A_TOKEN_FULL_ACCESS 92 | 94 | octokit-fixture-user-a 95 | 97 | All scopes enabled 98 |
102 | 2 103 | 105 | FIXTURES_USER_B_TOKEN_FULL_ACCESS 106 | 108 | octokit-fixture-user-b 109 | 111 | All scopes enabled 112 |
115 | 116 | ## Record 117 | 118 | Run scenarios from `scenarios/**` against the real GitHub APIs and compare 119 | responses to previously recorded fixtures 120 | 121 | ``` 122 | node bin/record 123 | ``` 124 | 125 | If there are changes, you can updated the recorded fixtures 126 | 127 | ``` 128 | node bin/record --update 129 | ``` 130 | 131 | To record selected scenarios, pass their names to the record command. 132 | You can combine that with the `--update` flag. 133 | 134 | ``` 135 | node bin/record api.github.com/get-root api.github.com/get-repository 136 | ``` 137 | 138 | In case you created temporary repositories that you want to delete all at once: 139 | 140 | ``` 141 | node bin/remove-temporary-repositories 142 | ``` 143 | 144 | ## Tests 145 | 146 | Run integration & unit tests with 147 | 148 | ``` 149 | npm test 150 | ``` 151 | 152 | ## Coverage 153 | 154 | After running tests, a coverage report can be generated that can be browsed locally. 155 | 156 | ``` 157 | npm run coverage 158 | ``` 159 | 160 | ## Releases 161 | 162 | Releases are automated using [semantic-release](https://github.com/semantic-release/semantic-release). 163 | The following commit message conventions determine which version is released: 164 | 165 | 1. `fix: ...` or `fix(scope name): ...` prefix in subject: bumps fix version, e.g. `1.2.3` → `1.2.4` 166 | 2. `feat: ...` or `feat(scope name): ...` prefix in subject: bumps feature version, e.g. `1.2.3` → `1.3.0` 167 | 3. `BREAKING CHANGE:` in body: bumps breaking version, e.g. `1.2.3` → `2.0.0` 168 | 169 | Only one version number is bumped at a time, the highest version change trumps the others. 170 | -------------------------------------------------------------------------------- /scenarios/api.github.com/branch-protection/record.js: -------------------------------------------------------------------------------- 1 | export default branchProtection; 2 | import env from "../../../lib/env.js"; 3 | import getTemporaryRepository from "../../../lib/temporary-repository.js"; 4 | 5 | async function branchProtection(state) { 6 | let error; 7 | // create a temporary repository 8 | const temporaryRepository = getTemporaryRepository({ 9 | request: state.request, 10 | token: env.FIXTURES_USER_A_TOKEN_FULL_ACCESS, 11 | org: "octokit-fixture-org", 12 | name: "branch-protection", 13 | }); 14 | 15 | await temporaryRepository.create(); 16 | 17 | try { 18 | // https://developer.github.com/v3/repos/contents/#create-a-file 19 | // (this request does not get recorded, we need an existing commit to set status on) 20 | await state.request({ 21 | method: "put", 22 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/contents/README.md`, 23 | headers: { 24 | Accept: "application/vnd.github.v3+json", 25 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 26 | "X-Octokit-Fixture-Ignore": "true", 27 | }, 28 | data: { 29 | message: "initial commit", 30 | content: Buffer.from("# branch-protection").toString("base64"), 31 | }, 32 | }); 33 | 34 | // https://developer.github.com/v3/orgs/teams/#add-or-update-team-repository 35 | // (this request does not get recorded, the team must be added to the 36 | // repository in order for it to be add to branch restrictions) 37 | // 2527061 is the ID of https://github.com/orgs/octokit-fixture-org/teams/a-team 38 | await state.request({ 39 | method: "put", 40 | url: `/teams/2527061/repos/octokit-fixture-org/${temporaryRepository.name}`, 41 | headers: { 42 | Accept: "application/vnd.github.v3+json", 43 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 44 | "X-Octokit-Fixture-Ignore": "true", 45 | }, 46 | data: { 47 | permission: "push", 48 | }, 49 | }); 50 | 51 | // https://developer.github.com/v3/repos/branches/#get-branch-protection 52 | // Get branch protection 53 | await state 54 | .request({ 55 | method: "get", 56 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/branches/main/protection`, 57 | headers: { 58 | Accept: "application/vnd.github.v3+json", 59 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 60 | }, 61 | }) 62 | .catch((error) => { 63 | if (error.response.data.message === "Branch not protected") { 64 | return; // this 404 is expected 65 | } 66 | 67 | throw error; 68 | }); 69 | 70 | // https://developer.github.com/v3/repos/branches/#update-branch-protection 71 | // Update branch protection with minimal settings 72 | await state.request({ 73 | method: "put", 74 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/branches/main/protection`, 75 | headers: { 76 | Accept: "application/vnd.github.v3+json", 77 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 78 | }, 79 | data: { 80 | required_status_checks: null, 81 | required_pull_request_reviews: null, 82 | restrictions: null, 83 | enforce_admins: false, 84 | }, 85 | }); 86 | 87 | // https://developer.github.com/v3/repos/branches/#update-branch-protection 88 | // Update branch protection with maximal settings 89 | await state.request({ 90 | method: "put", 91 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/branches/main/protection`, 92 | headers: { 93 | Accept: "application/vnd.github.v3+json", 94 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 95 | }, 96 | data: { 97 | required_status_checks: { 98 | strict: true, 99 | contexts: ["foo/bar"], 100 | }, 101 | required_pull_request_reviews: { 102 | dismissal_restrictions: { 103 | users: ["octokit-fixture-user-a"], 104 | teams: [], // bug: server returns "Only 100 users and teams can be specified." when set to ['a-team'] 105 | }, 106 | dismiss_stale_reviews: true, 107 | require_code_owner_reviews: false, 108 | }, 109 | restrictions: { 110 | users: ["octokit-fixture-user-a"], 111 | teams: ["a-team"], 112 | }, 113 | enforce_admins: true, 114 | }, 115 | }); 116 | 117 | // https://developer.github.com/v3/repos/branches/#remove-branch-protection 118 | // Remove branch protection 119 | await state.request({ 120 | method: "delete", 121 | url: `/repos/octokit-fixture-org/${temporaryRepository.name}/branches/main/protection`, 122 | headers: { 123 | Accept: "application/vnd.github.v3+json", 124 | Authorization: `token ${env.FIXTURES_USER_A_TOKEN_FULL_ACCESS}`, 125 | }, 126 | }); 127 | } catch (_error) { 128 | error = _error; 129 | } 130 | 131 | // await temporaryRepository.delete(); 132 | 133 | if (error) { 134 | return Promise.reject(error); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/integration/additions.test.js: -------------------------------------------------------------------------------- 1 | import isObject from "lodash/isObject.js"; 2 | import mapValues from "lodash/mapValues.js"; 3 | import axios from "axios"; 4 | 5 | import fixtures from "../../index.js"; 6 | 7 | test("reqheaders additions", async () => { 8 | const mock = fixtures.mock("api.github.com/get-repository", { 9 | reqheaders: { 10 | "x-fixtures-id": "123", 11 | }, 12 | }); 13 | 14 | try { 15 | await axios({ 16 | method: "get", 17 | url: "https://api.github.com/repos/octokit-fixture-org/hello-world", 18 | headers: { 19 | Accept: "application/vnd.github.v3+json", 20 | Authorization: "token 0000000000000000000000000000000000000001", 21 | }, 22 | }); 23 | throw new Error("should fail without X-Foo header"); 24 | } catch (error) { 25 | expect(error.message).toMatch("No match for request"); 26 | } 27 | 28 | try { 29 | await axios({ 30 | method: "get", 31 | url: "https://api.github.com/repos/octokit-fixture-org/hello-world", 32 | headers: { 33 | Accept: "application/vnd.github.v3+json", 34 | Authorization: "token 0000000000000000000000000000000000000001", 35 | "x-fixtures-id": "123", 36 | }, 37 | }); 38 | } catch (error) { 39 | throw new Error(mock.explain(error)); 40 | } 41 | }); 42 | 43 | test("scope additions", async () => { 44 | const mock = fixtures.mock("api.github.com/rename-repository", { 45 | scope: "http://localhost:3000", 46 | }); 47 | 48 | // https://developer.github.com/v3/repos/#edit 49 | await axios({ 50 | method: "patch", 51 | url: "http://localhost:3000/repos/octokit-fixture-org/rename-repository", 52 | headers: { 53 | Accept: "application/vnd.github.v3+json", 54 | Authorization: "token 0000000000000000000000000000000000000001", 55 | "Content-Type": "application/json; charset=utf-8", 56 | }, 57 | data: { 58 | name: "rename-repository-newname", 59 | }, 60 | }).catch((error) => expect(mock.explain(error)).toBeFalsy()); 61 | 62 | // https://developer.github.com/v3/repos/#get 63 | await axios({ 64 | method: "get", 65 | url: "http://localhost:3000/repos/octokit-fixture-org/rename-repository", 66 | headers: { 67 | Accept: "application/vnd.github.v3+json", 68 | Authorization: "token 0000000000000000000000000000000000000001", 69 | }, 70 | maxRedirects: 0, 71 | }).catch((error) => { 72 | if (/No match for request/.test(error.message)) { 73 | expect(mock.explain(error)).toBeFalsy(); 74 | } 75 | 76 | expect(error.response.headers.location).toBe( 77 | "http://localhost:3000/repositories/1000", 78 | ); 79 | }); 80 | }); 81 | 82 | test("additions function", async () => { 83 | const mapValuesDeep = (v, callback) => 84 | isObject(v) ? mapValues(v, (v) => mapValuesDeep(v, callback)) : callback(v); 85 | const mock = fixtures.mock("api.github.com/release-assets", (fixture) => { 86 | if (fixture.scope === "https://uploads.github.com:443") { 87 | fixture.path = `/uploads.github.com${fixture.path}`; 88 | } 89 | 90 | fixture.scope = "http://localhost:3000"; 91 | fixture.reqheaders.host = "localhost:3000"; 92 | fixture.reqheaders["x-fixtures-id"] = "123"; 93 | return mapValuesDeep(fixture, (value) => { 94 | if (typeof value !== "string") { 95 | return value; 96 | } 97 | 98 | return value 99 | .replace("https://api.github.com/", "http://localhost:3000/") 100 | .replace( 101 | "https://uploads.github.com/", 102 | "http://localhost:3000/uploads.github.com/", 103 | ); 104 | }); 105 | }); 106 | 107 | // https://developer.github.com/v3/repos/releases/#upload-a-release-asset 108 | // Get release to retrieve upload URL 109 | const { data } = await axios({ 110 | method: "get", 111 | url: "http://localhost:3000/repos/octokit-fixture-org/release-assets/releases/tags/v1.0.0", 112 | headers: { 113 | Accept: "application/vnd.github.v3+json", 114 | Authorization: "token 0000000000000000000000000000000000000001", 115 | "x-fixtures-id": "123", 116 | }, 117 | }).catch((error) => expect(mock.explain(error)).toBeFalsy()); 118 | 119 | expect(data.url).toBe( 120 | "http://localhost:3000/repos/octokit-fixture-org/release-assets/releases/1000", 121 | ); 122 | expect(data.upload_url).toBe( 123 | "http://localhost:3000/uploads.github.com/repos/octokit-fixture-org/release-assets/releases/1000/assets{?name,label}", 124 | ); 125 | 126 | // https://developer.github.com/v3/repos/releases/#upload-a-release-asset 127 | // upload attachment to release URL returned by create release request 128 | await axios({ 129 | method: "post", 130 | url: "http://localhost:3000/uploads.github.com/repos/octokit-fixture-org/release-assets/releases/1000/assets?name=test-upload.txt&label=test", 131 | headers: { 132 | Accept: "application/vnd.github.v3+json", 133 | Authorization: "token 0000000000000000000000000000000000000001", 134 | "Content-Type": "text/plain", 135 | "Content-Length": 14, 136 | "x-fixtures-id": "123", 137 | }, 138 | data: "Hello, world!\n", 139 | }).catch((error) => console.log(mock.explain(error))); 140 | }); 141 | -------------------------------------------------------------------------------- /scenarios/api.github.com/get-content/normalized-fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/repos/octokit-fixture-org/hello-world/contents/", 6 | "body": "", 7 | "status": 200, 8 | "response": [ 9 | { 10 | "name": "README.md", 11 | "path": "README.md", 12 | "sha": "93a078d1c3f76aa1ca11def8f882a06df1d4a01b", 13 | "size": 13, 14 | "url": "https://api.github.com/repos/octokit-fixture-org/hello-world/contents/README.md?ref=master", 15 | "html_url": "https://github.com/octokit-fixture-org/hello-world/blob/master/README.md", 16 | "git_url": "https://api.github.com/repos/octokit-fixture-org/hello-world/git/blobs/93a078d1c3f76aa1ca11def8f882a06df1d4a01b", 17 | "download_url": "https://raw.githubusercontent.com/octokit-fixture-org/hello-world/master/README.md", 18 | "type": "file", 19 | "_links": { 20 | "self": "https://api.github.com/repos/octokit-fixture-org/hello-world/contents/README.md?ref=master", 21 | "git": "https://api.github.com/repos/octokit-fixture-org/hello-world/git/blobs/93a078d1c3f76aa1ca11def8f882a06df1d4a01b", 22 | "html": "https://github.com/octokit-fixture-org/hello-world/blob/master/README.md" 23 | } 24 | } 25 | ], 26 | "reqheaders": { 27 | "accept": "application/vnd.github.v3+json", 28 | "authorization": "token 0000000000000000000000000000000000000001", 29 | "host": "api.github.com" 30 | }, 31 | "responseIsBinary": false, 32 | "headers": { 33 | "access-control-allow-origin": "*", 34 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 35 | "cache-control": "private, max-age=60, s-maxage=60", 36 | "connection": "close", 37 | "content-length": "836", 38 | "content-security-policy": "default-src 'none'", 39 | "content-type": "application/json; charset=utf-8", 40 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 41 | "etag": "\"00000000000000000000000000000000\"", 42 | "last-modified": "Tue, 10 Oct 2017 16:00:00 GMT", 43 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 44 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 45 | "x-accepted-oauth-scopes": "", 46 | "x-content-type-options": "nosniff", 47 | "x-frame-options": "deny", 48 | "x-github-media-type": "github.v3; format=json", 49 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 50 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 51 | "x-ratelimit-limit": "5000", 52 | "x-ratelimit-remaining": "4999", 53 | "x-ratelimit-reset": "1507651200000", 54 | "x-ratelimit-resource": "core", 55 | "x-ratelimit-used": 1, 56 | "x-xss-protection": "0" 57 | } 58 | }, 59 | { 60 | "scope": "https://api.github.com:443", 61 | "method": "get", 62 | "path": "/repos/octokit-fixture-org/hello-world/contents/README.md", 63 | "body": "", 64 | "status": 200, 65 | "response": "# hello-world", 66 | "reqheaders": { 67 | "accept": "application/vnd.github.v3.raw", 68 | "authorization": "token 0000000000000000000000000000000000000001", 69 | "host": "api.github.com" 70 | }, 71 | "responseIsBinary": false, 72 | "headers": { 73 | "access-control-allow-origin": "*", 74 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 75 | "cache-control": "private, max-age=60, s-maxage=60", 76 | "connection": "close", 77 | "content-length": "13", 78 | "content-security-policy": "default-src 'none'", 79 | "content-type": "application/vnd.github.v3.raw; charset=utf-8", 80 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 81 | "etag": "\"00000000000000000000000000000000\"", 82 | "last-modified": "Tue, 10 Oct 2017 16:00:00 GMT", 83 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 84 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 85 | "x-accepted-oauth-scopes": "", 86 | "x-content-type-options": "nosniff", 87 | "x-frame-options": "deny", 88 | "x-github-media-type": "github.v3; param=raw", 89 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 90 | "x-oauth-scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, workflow", 91 | "x-ratelimit-limit": "5000", 92 | "x-ratelimit-remaining": "4999", 93 | "x-ratelimit-reset": "1507651200000", 94 | "x-ratelimit-resource": "core", 95 | "x-ratelimit-used": 1, 96 | "x-xss-protection": "0" 97 | } 98 | } 99 | ] 100 | --------------------------------------------------------------------------------