diff --git a/tests/index.js b/tests/index.js index fd8dbd0..5d48164 100644 --- a/tests/index.js +++ b/tests/index.js @@ -7,7 +7,12 @@ const tests = readdirSync("tests").filter((file) => file.endsWith(".test.js")); for (const file of tests) { test(file, async (t) => { - const { stderr, stdout } = await execa("node", [`tests/${file}`]); + // Override Actions environment variables that change `core`’s behavior + const env = { + GITHUB_OUTPUT: undefined, + GITHUB_STATE: undefined, + }; + const { stderr, stdout } = await execa("node", [`tests/${file}`], { env }); t.snapshot(stderr, "stderr"); t.snapshot(stdout, "stdout"); }); diff --git a/tests/main-missing-owner.test.js b/tests/main-missing-owner.test.js new file mode 100644 index 0000000..e19542b --- /dev/null +++ b/tests/main-missing-owner.test.js @@ -0,0 +1,9 @@ +process.env.GITHUB_REPOSITORY = "actions/create-github-app-token"; +delete process.env.GITHUB_REPOSITORY_OWNER; + +// Verify `main` exits with an error when `GITHUB_REPOSITORY_OWNER` is missing. +try { + await import("../main.js"); +} catch (error) { + console.error(error.message); +} diff --git a/tests/main-missing-repository.test.js b/tests/main-missing-repository.test.js new file mode 100644 index 0000000..8496eab --- /dev/null +++ b/tests/main-missing-repository.test.js @@ -0,0 +1,8 @@ +delete process.env.GITHUB_REPOSITORY; + +// Verify `main` exits with an error when `GITHUB_REPOSITORY` is missing. +try { + await import("../main.js"); +} catch (error) { + console.error(error.message); +} diff --git a/tests/main-token-get-owner-set-repo-set-to-many.test.js b/tests/main-token-get-owner-set-repo-set-to-many.test.js new file mode 100644 index 0000000..fa18b1a --- /dev/null +++ b/tests/main-token-get-owner-set-repo-set-to-many.test.js @@ -0,0 +1,7 @@ +import { test } from "./main.js"; + +// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a list of repos). +await test(() => { + process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; + process.env.INPUT_REPOSITORIES = `${process.env.GITHUB_REPOSITORY},actions/toolkit`; +}); diff --git a/tests/main-token-get-owner-set-repo-set-to-one.test.js b/tests/main-token-get-owner-set-repo-set-to-one.test.js new file mode 100644 index 0000000..3e0f733 --- /dev/null +++ b/tests/main-token-get-owner-set-repo-set-to-one.test.js @@ -0,0 +1,7 @@ +import { test } from "./main.js"; + +// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a single repo). +await test(() => { + process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; + process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY; +}); diff --git a/tests/main-token-get-owner-set-to-org-repo-unset.test.js b/tests/main-token-get-owner-set-to-org-repo-unset.test.js new file mode 100644 index 0000000..1fa0e1f --- /dev/null +++ b/tests/main-token-get-owner-set-to-org-repo-unset.test.js @@ -0,0 +1,25 @@ +import { test } from "./main.js"; + +// Verify `main` successfully obtains a token when the `owner` input is set (to an org), but the `repositories` input isn’t set. +await test((mockPool) => { + process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; + delete process.env.INPUT_REPOSITORIES; + + // Mock installation id request + const mockInstallationId = "123456"; + mockPool + .intercept({ + path: `/orgs/${process.env.INPUT_OWNER}/installation`, + method: "GET", + headers: { + accept: "application/vnd.github.v3+json", + "user-agent": "actions/create-github-app-token", + // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. + }, + }) + .reply( + 200, + { id: mockInstallationId }, + { headers: { "content-type": "application/json" } } + ); +}); diff --git a/tests/main-token-get-owner-set-to-user-repo-unset.test.js b/tests/main-token-get-owner-set-to-user-repo-unset.test.js new file mode 100644 index 0000000..bbb802c --- /dev/null +++ b/tests/main-token-get-owner-set-to-user-repo-unset.test.js @@ -0,0 +1,36 @@ +import { test } from "./main.js"; + +// Verify `main` successfully obtains a token when the `owner` input is set (to a user), but the `repositories` input isn’t set. +await test((mockPool) => { + process.env.INPUT_OWNER = "smockle"; + delete process.env.INPUT_REPOSITORIES; + + // Mock installation id request + const mockInstallationId = "123456"; + mockPool + .intercept({ + path: `/orgs/${process.env.INPUT_OWNER}/installation`, + method: "GET", + headers: { + accept: "application/vnd.github.v3+json", + "user-agent": "actions/create-github-app-token", + // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. + }, + }) + .reply(404); + mockPool + .intercept({ + path: `/users/${process.env.INPUT_OWNER}/installation`, + method: "GET", + headers: { + accept: "application/vnd.github.v3+json", + "user-agent": "actions/create-github-app-token", + // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. + }, + }) + .reply( + 200, + { id: mockInstallationId }, + { headers: { "content-type": "application/json" } } + ); +}); diff --git a/tests/main-token-get-owner-unset-repo-set.test.js b/tests/main-token-get-owner-unset-repo-set.test.js new file mode 100644 index 0000000..89d6a85 --- /dev/null +++ b/tests/main-token-get-owner-unset-repo-set.test.js @@ -0,0 +1,7 @@ +import { test } from "./main.js"; + +// Verify `main` successfully obtains a token when the `owner` input is not set, but the `repositories` input is set. +await test(() => { + delete process.env.INPUT_OWNER; + process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY; +}); diff --git a/tests/main-token-get-owner-unset-repo-unset.test.js b/tests/main-token-get-owner-unset-repo-unset.test.js new file mode 100644 index 0000000..ccc20dd --- /dev/null +++ b/tests/main-token-get-owner-unset-repo-unset.test.js @@ -0,0 +1,25 @@ +import { test } from "./main.js"; + +// Verify `main` successfully obtains a token when neither the `owner` nor `repositories` input is set. +await test((mockPool) => { + delete process.env.INPUT_OWNER; + delete process.env.INPUT_REPOSITORIES; + + // Mock installation id request + const mockInstallationId = "123456"; + mockPool + .intercept({ + path: `/repos/${process.env.GITHUB_REPOSITORY}/installation`, + method: "GET", + headers: { + accept: "application/vnd.github.v3+json", + "user-agent": "actions/create-github-app-token", + // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. + }, + }) + .reply( + 200, + { id: mockInstallationId }, + { headers: { "content-type": "application/json" } } + ); +}); diff --git a/tests/main.js b/tests/main.js new file mode 100644 index 0000000..9e62af8 --- /dev/null +++ b/tests/main.js @@ -0,0 +1,96 @@ +// Base for all `main` tests. +// @ts-check +import { MockAgent, setGlobalDispatcher } from "undici"; + +export async function test(cb = (_mockPool) => {}) { + // Set required environment variables and inputs + process.env.GITHUB_REPOSITORY_OWNER = "actions"; + process.env.GITHUB_REPOSITORY = "actions/create-github-app-token"; + // inputs are set as environment variables with the prefix INPUT_ + // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs + process.env.INPUT_APP_ID = "123456"; + process.env.INPUT_PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA280nfuUM9w00Ib9E2rvZJ6Qu3Ua3IqR34ZlK53vn/Iobn2EL +Z9puc5Q/nFBU15NKwHyQNb+OG2hTCkjd1Xi9XPzEOH1r42YQmTGq8YCkUSkk6KZA +5dnhLwN9pFquT9fQgrf4r1D5GJj3rqvj8JDr1sBmunArqY5u4gziSrIohcjLIZV0 +cIMz/RUIMe/EAsNeiwzEteHAtf/WpMs+OfF94SIUrDlkPr0H0H3DER8l1HZAvE0e +eD3ZJ6njrF6UHQWDVrekSTB0clpVTTU9TMpe+gs2nnFww9G8As+WsW8xHVjVipJy +AwqBhiR+s7wlcbh2i0NQqt8GL9/jIFTmleiwsQIDAQABAoIBAHNyP8pgl/yyzKzk +/0871wUBMTQ7zji91dGCaFtJM0HrcDK4D/uOOPEv7nE1qDpKPLr5Me1pHUS7+NGw +EAPtlNhgUtew2JfppdIwyi5qeOPADoi7ud6AH8xHsxg+IMwC+JuP8WhzyUHoJj9y +PRi/pX94Mvy9qdE25HqKddjx1mLdaHhxqPkr16/em23uYZqm1lORsCPD3vTlthj7 +WiEbBSqmpYvjj8iFP4yFk2N+LvuWgSilRzq1Af3qE7PUp4xhjmcOPs78Sag1T7nl +ww/pgqCegISABHik7j++/5T+UlI5cLsyp/XENU9zAd4kCIczwNKpun2bn+djJdft +ravyX4ECgYEA+k2mHfi1zwKF3wT+cJbf30+uXrJczK2yZ33//4RKnhBaq7nSbQAI +nhWz2JESBK0TEo0+L7yYYq3HnT9vcES5R1NxzruH9wXgxssSx3JUj6w1raXYPh3B ++1XpYQsa6/bo2LmBELEx47F8FQbNgD5dmTJ4jBrf6MV4rRh9h6Bs7UkCgYEA4M3K +eAw52c2MNMIxH/LxdOQtEBq5GMu3AQC8I64DSSRrAoiypfEgyTV6S4gWJ5TKgYfD +zclnOVJF+tITe3neO9wHoZp8iCjCnoijcT6p2cJ4IaW68LEHPOokWBk0EpLjF4p2 +7sFi9+lUpXYXfCN7aMJ77QpGzB7dNsBf0KUxMCkCgYEAjw/mjGbk82bLwUaHby6s +0mQmk7V6WPpGZ+Sadx7TzzglutVAslA8nK5m1rdEBywtJINaMcqnhm8xEm15cj+1 +blEBUVnaQpQ3fyf+mcR9FIknPRL3X7l+b/sQowjH4GqFd6m/XR0KGMwO0a3Lsyry +MGeqgtmxdMe5S6YdyXEmERECgYAgQsgklDSVIh9Vzux31kh6auhgoEUh3tJDbZSS +Vj2YeIZ21aE1mTYISglj34K2aW7qSc56sMWEf18VkKJFHQccdgYOVfo7HAZZ8+fo +r4J2gqb0xTDfq7gLMNrIXc2QQM4gKbnJp60JQM3p9NmH8huavBZGvSvNzTwXyGG3 +so0tiQKBgGQXZaxaXhYUcxYHuCkQ3V4Vsj3ezlM92xXlP32SGFm3KgFhYy9kATxw +Cax1ytZzvlrKLQyQFVK1COs2rHt7W4cJ7op7C8zXfsigXCiejnS664oAuX8sQZID +x3WQZRiXlWejSMUAHuMwXrhGlltF3lw83+xAjnqsVp75kGS6OH61 +-----END RSA PRIVATE KEY-----`; // This key is invalidated. It’s from https://github.com/octokit/auth-app.js/issues/465#issuecomment-1564998327. + + // Set up mocking + const mockAgent = new MockAgent(); + mockAgent.disableNetConnect(); + setGlobalDispatcher(mockAgent); + const mockPool = mockAgent.get("https://api.github.com"); + + // Calling `auth({ type: "app" })` to obtain a JWT doesn’t make network requests, so no need to intercept. + + // Mock installation id request + const mockInstallationId = "123456"; + const owner = process.env.INPUT_OWNER ?? process.env.GITHUB_REPOSITORY_OWNER; + const repo = encodeURIComponent( + (process.env.INPUT_REPOSITORIES ?? process.env.GITHUB_REPOSITORY).split( + "," + )[0] + ); + mockPool + .intercept({ + path: `/repos/${owner}/${repo}/installation`, + method: "GET", + headers: { + accept: "application/vnd.github.v3+json", + "user-agent": "actions/create-github-app-token", + // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. + }, + }) + .reply( + 200, + { id: mockInstallationId }, + { headers: { "content-type": "application/json" } } + ); + + // Mock installation access token request + const mockInstallationAccessToken = + "ghs_16C7e42F292c6912E7710c838347Ae178B4a"; // This token is invalidated. It’s from https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app. + mockPool + .intercept({ + path: `/app/installations/${mockInstallationId}/access_tokens`, + method: "POST", + headers: { + accept: "application/vnd.github.v3+json", + "user-agent": "actions/create-github-app-token", + // Note: Intentionally omitting the `authorization` header, since JWT creation is not idempotent. + }, + }) + .reply( + 201, + { token: mockInstallationAccessToken }, + { headers: { "content-type": "application/json" } } + ); + + // Run the callback + cb(mockPool); + + // Run the main script + await import("../main.js"); +} diff --git a/tests/snapshots/index.js.md b/tests/snapshots/index.js.md index 1f0debc..4ab4701 100644 --- a/tests/snapshots/index.js.md +++ b/tests/snapshots/index.js.md @@ -4,6 +4,110 @@ The actual snapshot is saved in `index.js.snap`. Generated by [AVA](https://avajs.dev). +## main-missing-owner.test.js + +> stderr + + 'GITHUB_REPOSITORY_OWNER missing, must be set to \'\'' + +> stdout + + '' + +## main-missing-repository.test.js + +> stderr + + 'GITHUB_REPOSITORY missing, must be set to \'/\'' + +> stdout + + '' + +## main-token-get-owner-set-repo-set-to-many.test.js + +> stderr + + '' + +> stdout + + `owner and repositories set, creating token for repositories "actions/create-github-app-token,actions/toolkit" owned by "actions"␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` + +## main-token-get-owner-set-repo-set-to-one.test.js + +> stderr + + '' + +> stdout + + `owner and repositories set, creating token for repositories "actions/create-github-app-token" owned by "actions"␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` + +## main-token-get-owner-set-to-org-repo-unset.test.js + +> stderr + + '' + +> stdout + + `repositories not set, creating token for all repositories for given owner "actions"␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` + +## main-token-get-owner-set-to-user-repo-unset.test.js + +> stderr + + '' + +> stdout + + `repositories not set, creating token for all repositories for given owner "smockle"␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` + +## main-token-get-owner-unset-repo-set.test.js + +> stderr + + '' + +> stdout + + `owner not set, creating owner for given repositories "actions/create-github-app-token" in current owner ("actions")␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` + +## main-token-get-owner-unset-repo-unset.test.js + +> stderr + + '' + +> stdout + + `owner and repositories not set, creating token for the current repository ("create-github-app-token")␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` + ## post-token-set.test.js > stderr diff --git a/tests/snapshots/index.js.snap b/tests/snapshots/index.js.snap index 954c16d..fb12409 100644 Binary files a/tests/snapshots/index.js.snap and b/tests/snapshots/index.js.snap differ