Skip to content

Commit

Permalink
test: integration tests for main.js (#56)
Browse files Browse the repository at this point in the history
Part of #43

This PR adds tests for
[`main.js`](https://github.com/actions/create-github-app-token/blob/main/lib/main.js),
similar to [the tests that already exist for
`post.js`](https://github.com/actions/create-github-app-token/tree/main/tests).

Specifically, it tests that:
- `main` exits with an error when `GITHUB_REPOSITORY` is missing.
- `main` exits with an error when `GITHUB_REPOSITORY_OWNER` is missing.
- `main` successfully obtains a token when…
- …the `owner` and `repositories` inputs are set (and the latter is a
single repo).
- …the `owner` and `repositories` inputs are set (and the latter is a
list of repos).
- …the `owner` input is set (to an org), but the `repositories` input
isn’t set.
- …the `owner` input is set (to a user), but the `repositories` input
isn’t set.
  - …the `owner` input is not set, but the `repositories` input is set.
  - …neither the `owner` nor `repositories` input is set.

❧

Architecturally, in order to keep individual tests concise, this PR adds
`tests/main.js`, which:
- sets commonly-used inputs, environment variables, and mocks, then
- calls a callback function that can edit the variables and add
additional mocks, then
- runs `main.js` itself.

The `tests/main-token-get-*.test.js` test files run `tests/main.js` with
various scenario-specific callback functions.
  • Loading branch information
smockle authored Oct 6, 2023
1 parent 8210939 commit 9b28355
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 1 deletion.
7 changes: 6 additions & 1 deletion tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
9 changes: 9 additions & 0 deletions tests/main-missing-owner.test.js
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 8 additions & 0 deletions tests/main-missing-repository.test.js
Original file line number Diff line number Diff line change
@@ -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);
}
7 changes: 7 additions & 0 deletions tests/main-token-get-owner-set-repo-set-to-many.test.js
Original file line number Diff line number Diff line change
@@ -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`;
});
7 changes: 7 additions & 0 deletions tests/main-token-get-owner-set-repo-set-to-one.test.js
Original file line number Diff line number Diff line change
@@ -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;
});
25 changes: 25 additions & 0 deletions tests/main-token-get-owner-set-to-org-repo-unset.test.js
Original file line number Diff line number Diff line change
@@ -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" } }
);
});
36 changes: 36 additions & 0 deletions tests/main-token-get-owner-set-to-user-repo-unset.test.js
Original file line number Diff line number Diff line change
@@ -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" } }
);
});
7 changes: 7 additions & 0 deletions tests/main-token-get-owner-unset-repo-set.test.js
Original file line number Diff line number Diff line change
@@ -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;
});
25 changes: 25 additions & 0 deletions tests/main-token-get-owner-unset-repo-unset.test.js
Original file line number Diff line number Diff line change
@@ -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" } }
);
});
96 changes: 96 additions & 0 deletions tests/main.js
Original file line number Diff line number Diff line change
@@ -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");
}
104 changes: 104 additions & 0 deletions tests/snapshots/index.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \'<owner>\''

> stdout
''

## main-missing-repository.test.js

> stderr
'GITHUB_REPOSITORY missing, must be set to \'<owner>/<repo>\''

> 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
Expand Down
Binary file modified tests/snapshots/index.js.snap
Binary file not shown.

0 comments on commit 9b28355

Please sign in to comment.