Skip to content

Commit 9b28355

Browse files
authored
test: integration tests for main.js (actions#56)
Part of actions#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.
1 parent 8210939 commit 9b28355

12 files changed

+330
-1
lines changed

tests/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ const tests = readdirSync("tests").filter((file) => file.endsWith(".test.js"));
77

88
for (const file of tests) {
99
test(file, async (t) => {
10-
const { stderr, stdout } = await execa("node", [`tests/${file}`]);
10+
// Override Actions environment variables that change `core`’s behavior
11+
const env = {
12+
GITHUB_OUTPUT: undefined,
13+
GITHUB_STATE: undefined,
14+
};
15+
const { stderr, stdout } = await execa("node", [`tests/${file}`], { env });
1116
t.snapshot(stderr, "stderr");
1217
t.snapshot(stdout, "stdout");
1318
});

tests/main-missing-owner.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
process.env.GITHUB_REPOSITORY = "actions/create-github-app-token";
2+
delete process.env.GITHUB_REPOSITORY_OWNER;
3+
4+
// Verify `main` exits with an error when `GITHUB_REPOSITORY_OWNER` is missing.
5+
try {
6+
await import("../main.js");
7+
} catch (error) {
8+
console.error(error.message);
9+
}

tests/main-missing-repository.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
delete process.env.GITHUB_REPOSITORY;
2+
3+
// Verify `main` exits with an error when `GITHUB_REPOSITORY` is missing.
4+
try {
5+
await import("../main.js");
6+
} catch (error) {
7+
console.error(error.message);
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a list of repos).
4+
await test(() => {
5+
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
6+
process.env.INPUT_REPOSITORIES = `${process.env.GITHUB_REPOSITORY},actions/toolkit`;
7+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a single repo).
4+
await test(() => {
5+
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
6+
process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY;
7+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when the `owner` input is set (to an org), but the `repositories` input isn’t set.
4+
await test((mockPool) => {
5+
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
6+
delete process.env.INPUT_REPOSITORIES;
7+
8+
// Mock installation id request
9+
const mockInstallationId = "123456";
10+
mockPool
11+
.intercept({
12+
path: `/orgs/${process.env.INPUT_OWNER}/installation`,
13+
method: "GET",
14+
headers: {
15+
accept: "application/vnd.github.v3+json",
16+
"user-agent": "actions/create-github-app-token",
17+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
18+
},
19+
})
20+
.reply(
21+
200,
22+
{ id: mockInstallationId },
23+
{ headers: { "content-type": "application/json" } }
24+
);
25+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when the `owner` input is set (to a user), but the `repositories` input isn’t set.
4+
await test((mockPool) => {
5+
process.env.INPUT_OWNER = "smockle";
6+
delete process.env.INPUT_REPOSITORIES;
7+
8+
// Mock installation id request
9+
const mockInstallationId = "123456";
10+
mockPool
11+
.intercept({
12+
path: `/orgs/${process.env.INPUT_OWNER}/installation`,
13+
method: "GET",
14+
headers: {
15+
accept: "application/vnd.github.v3+json",
16+
"user-agent": "actions/create-github-app-token",
17+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
18+
},
19+
})
20+
.reply(404);
21+
mockPool
22+
.intercept({
23+
path: `/users/${process.env.INPUT_OWNER}/installation`,
24+
method: "GET",
25+
headers: {
26+
accept: "application/vnd.github.v3+json",
27+
"user-agent": "actions/create-github-app-token",
28+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
29+
},
30+
})
31+
.reply(
32+
200,
33+
{ id: mockInstallationId },
34+
{ headers: { "content-type": "application/json" } }
35+
);
36+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when the `owner` input is not set, but the `repositories` input is set.
4+
await test(() => {
5+
delete process.env.INPUT_OWNER;
6+
process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY;
7+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when neither the `owner` nor `repositories` input is set.
4+
await test((mockPool) => {
5+
delete process.env.INPUT_OWNER;
6+
delete process.env.INPUT_REPOSITORIES;
7+
8+
// Mock installation id request
9+
const mockInstallationId = "123456";
10+
mockPool
11+
.intercept({
12+
path: `/repos/${process.env.GITHUB_REPOSITORY}/installation`,
13+
method: "GET",
14+
headers: {
15+
accept: "application/vnd.github.v3+json",
16+
"user-agent": "actions/create-github-app-token",
17+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
18+
},
19+
})
20+
.reply(
21+
200,
22+
{ id: mockInstallationId },
23+
{ headers: { "content-type": "application/json" } }
24+
);
25+
});

tests/main.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Base for all `main` tests.
2+
// @ts-check
3+
import { MockAgent, setGlobalDispatcher } from "undici";
4+
5+
export async function test(cb = (_mockPool) => {}) {
6+
// Set required environment variables and inputs
7+
process.env.GITHUB_REPOSITORY_OWNER = "actions";
8+
process.env.GITHUB_REPOSITORY = "actions/create-github-app-token";
9+
// inputs are set as environment variables with the prefix INPUT_
10+
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
11+
process.env.INPUT_APP_ID = "123456";
12+
process.env.INPUT_PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
13+
MIIEowIBAAKCAQEA280nfuUM9w00Ib9E2rvZJ6Qu3Ua3IqR34ZlK53vn/Iobn2EL
14+
Z9puc5Q/nFBU15NKwHyQNb+OG2hTCkjd1Xi9XPzEOH1r42YQmTGq8YCkUSkk6KZA
15+
5dnhLwN9pFquT9fQgrf4r1D5GJj3rqvj8JDr1sBmunArqY5u4gziSrIohcjLIZV0
16+
cIMz/RUIMe/EAsNeiwzEteHAtf/WpMs+OfF94SIUrDlkPr0H0H3DER8l1HZAvE0e
17+
eD3ZJ6njrF6UHQWDVrekSTB0clpVTTU9TMpe+gs2nnFww9G8As+WsW8xHVjVipJy
18+
AwqBhiR+s7wlcbh2i0NQqt8GL9/jIFTmleiwsQIDAQABAoIBAHNyP8pgl/yyzKzk
19+
/0871wUBMTQ7zji91dGCaFtJM0HrcDK4D/uOOPEv7nE1qDpKPLr5Me1pHUS7+NGw
20+
EAPtlNhgUtew2JfppdIwyi5qeOPADoi7ud6AH8xHsxg+IMwC+JuP8WhzyUHoJj9y
21+
PRi/pX94Mvy9qdE25HqKddjx1mLdaHhxqPkr16/em23uYZqm1lORsCPD3vTlthj7
22+
WiEbBSqmpYvjj8iFP4yFk2N+LvuWgSilRzq1Af3qE7PUp4xhjmcOPs78Sag1T7nl
23+
ww/pgqCegISABHik7j++/5T+UlI5cLsyp/XENU9zAd4kCIczwNKpun2bn+djJdft
24+
ravyX4ECgYEA+k2mHfi1zwKF3wT+cJbf30+uXrJczK2yZ33//4RKnhBaq7nSbQAI
25+
nhWz2JESBK0TEo0+L7yYYq3HnT9vcES5R1NxzruH9wXgxssSx3JUj6w1raXYPh3B
26+
+1XpYQsa6/bo2LmBELEx47F8FQbNgD5dmTJ4jBrf6MV4rRh9h6Bs7UkCgYEA4M3K
27+
eAw52c2MNMIxH/LxdOQtEBq5GMu3AQC8I64DSSRrAoiypfEgyTV6S4gWJ5TKgYfD
28+
zclnOVJF+tITe3neO9wHoZp8iCjCnoijcT6p2cJ4IaW68LEHPOokWBk0EpLjF4p2
29+
7sFi9+lUpXYXfCN7aMJ77QpGzB7dNsBf0KUxMCkCgYEAjw/mjGbk82bLwUaHby6s
30+
0mQmk7V6WPpGZ+Sadx7TzzglutVAslA8nK5m1rdEBywtJINaMcqnhm8xEm15cj+1
31+
blEBUVnaQpQ3fyf+mcR9FIknPRL3X7l+b/sQowjH4GqFd6m/XR0KGMwO0a3Lsyry
32+
MGeqgtmxdMe5S6YdyXEmERECgYAgQsgklDSVIh9Vzux31kh6auhgoEUh3tJDbZSS
33+
Vj2YeIZ21aE1mTYISglj34K2aW7qSc56sMWEf18VkKJFHQccdgYOVfo7HAZZ8+fo
34+
r4J2gqb0xTDfq7gLMNrIXc2QQM4gKbnJp60JQM3p9NmH8huavBZGvSvNzTwXyGG3
35+
so0tiQKBgGQXZaxaXhYUcxYHuCkQ3V4Vsj3ezlM92xXlP32SGFm3KgFhYy9kATxw
36+
Cax1ytZzvlrKLQyQFVK1COs2rHt7W4cJ7op7C8zXfsigXCiejnS664oAuX8sQZID
37+
x3WQZRiXlWejSMUAHuMwXrhGlltF3lw83+xAjnqsVp75kGS6OH61
38+
-----END RSA PRIVATE KEY-----`; // This key is invalidated. It’s from https://github.com/octokit/auth-app.js/issues/465#issuecomment-1564998327.
39+
40+
// Set up mocking
41+
const mockAgent = new MockAgent();
42+
mockAgent.disableNetConnect();
43+
setGlobalDispatcher(mockAgent);
44+
const mockPool = mockAgent.get("https://api.github.com");
45+
46+
// Calling `auth({ type: "app" })` to obtain a JWT doesn’t make network requests, so no need to intercept.
47+
48+
// Mock installation id request
49+
const mockInstallationId = "123456";
50+
const owner = process.env.INPUT_OWNER ?? process.env.GITHUB_REPOSITORY_OWNER;
51+
const repo = encodeURIComponent(
52+
(process.env.INPUT_REPOSITORIES ?? process.env.GITHUB_REPOSITORY).split(
53+
","
54+
)[0]
55+
);
56+
mockPool
57+
.intercept({
58+
path: `/repos/${owner}/${repo}/installation`,
59+
method: "GET",
60+
headers: {
61+
accept: "application/vnd.github.v3+json",
62+
"user-agent": "actions/create-github-app-token",
63+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
64+
},
65+
})
66+
.reply(
67+
200,
68+
{ id: mockInstallationId },
69+
{ headers: { "content-type": "application/json" } }
70+
);
71+
72+
// Mock installation access token request
73+
const mockInstallationAccessToken =
74+
"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.
75+
mockPool
76+
.intercept({
77+
path: `/app/installations/${mockInstallationId}/access_tokens`,
78+
method: "POST",
79+
headers: {
80+
accept: "application/vnd.github.v3+json",
81+
"user-agent": "actions/create-github-app-token",
82+
// Note: Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
83+
},
84+
})
85+
.reply(
86+
201,
87+
{ token: mockInstallationAccessToken },
88+
{ headers: { "content-type": "application/json" } }
89+
);
90+
91+
// Run the callback
92+
cb(mockPool);
93+
94+
// Run the main script
95+
await import("../main.js");
96+
}

tests/snapshots/index.js.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,110 @@ The actual snapshot is saved in `index.js.snap`.
44

55
Generated by [AVA](https://avajs.dev).
66

7+
## main-missing-owner.test.js
8+
9+
> stderr
10+
11+
'GITHUB_REPOSITORY_OWNER missing, must be set to \'<owner>\''
12+
13+
> stdout
14+
15+
''
16+
17+
## main-missing-repository.test.js
18+
19+
> stderr
20+
21+
'GITHUB_REPOSITORY missing, must be set to \'<owner>/<repo>\''
22+
23+
> stdout
24+
25+
''
26+
27+
## main-token-get-owner-set-repo-set-to-many.test.js
28+
29+
> stderr
30+
31+
''
32+
33+
> stdout
34+
35+
`owner and repositories set, creating token for repositories "actions/create-github-app-token,actions/toolkit" owned by "actions"␊
36+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
37+
38+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
39+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
40+
41+
## main-token-get-owner-set-repo-set-to-one.test.js
42+
43+
> stderr
44+
45+
''
46+
47+
> stdout
48+
49+
`owner and repositories set, creating token for repositories "actions/create-github-app-token" owned by "actions"␊
50+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
51+
52+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
53+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
54+
55+
## main-token-get-owner-set-to-org-repo-unset.test.js
56+
57+
> stderr
58+
59+
''
60+
61+
> stdout
62+
63+
`repositories not set, creating token for all repositories for given owner "actions"␊
64+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
65+
66+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
67+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
68+
69+
## main-token-get-owner-set-to-user-repo-unset.test.js
70+
71+
> stderr
72+
73+
''
74+
75+
> stdout
76+
77+
`repositories not set, creating token for all repositories for given owner "smockle"␊
78+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
79+
80+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
81+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
82+
83+
## main-token-get-owner-unset-repo-set.test.js
84+
85+
> stderr
86+
87+
''
88+
89+
> stdout
90+
91+
`owner not set, creating owner for given repositories "actions/create-github-app-token" in current owner ("actions")␊
92+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
93+
94+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
95+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
96+
97+
## main-token-get-owner-unset-repo-unset.test.js
98+
99+
> stderr
100+
101+
''
102+
103+
> stdout
104+
105+
`owner and repositories not set, creating token for the current repository ("create-github-app-token")␊
106+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
107+
108+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
109+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
110+
7111
## post-token-set.test.js
8112

9113
> stderr

tests/snapshots/index.js.snap

437 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)