Skip to content

Commit c832d6e

Browse files
authored
Add unit tests (#24)
* Add unit tests * Add GitHub Actions CI workflow * Add Prettier
1 parent 3bdc29c commit c832d6e

File tree

5 files changed

+8417
-41
lines changed

5 files changed

+8417
-41
lines changed

.github/workflows/ci.yml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Node.js CI
2+
3+
on: [push]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
9+
strategy:
10+
matrix:
11+
node-version: [14.x, 16.x, 18.x]
12+
13+
env:
14+
CI: true
15+
steps:
16+
- uses: actions/checkout@v2
17+
- name: Use Node.js ${{ matrix.node-version }}
18+
uses: actions/setup-node@v1
19+
with:
20+
node-version: ${{ matrix.node-version }}
21+
- run: npm ci
22+
- run: npm test
23+
- run: npm run lint

index.js

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
const { Toolkit } = require("actions-toolkit");
22

33
Toolkit.run(async (tools) => {
4+
// Process inputs for use later
45
const mode = tools.inputs.mode;
56
const count = parseInt(tools.inputs.count, 10);
67
const allowedLabels = tools.inputs.labels
78
.split(",")
89
.map((l) => l.trim())
910
.filter((r) => r);
1011

11-
if (allowedLabels.length === 0) {
12-
tools.exit.failure("Missing input 'labels'");
12+
// Validate inputs
13+
if (tools.inputs.count === "") {
14+
tools.exit.failure(`[count] input is not provided`);
1315
return;
1416
}
1517

16-
// Validation
17-
if (count === "") {
18-
tools.exit.failure(`Missing input.count`);
18+
if (allowedLabels.length === 0) {
19+
tools.exit.failure("[labels] input is empty or not provided");
1920
return;
2021
}
2122

2223
const allowedModes = ["exactly", "minimum", "maximum"];
2324
if (!allowedModes.includes(mode)) {
2425
tools.exit.failure(
25-
`Unknown input.mode '${mode}'. Must be one of: ${allowedModes.join(", ")}`
26+
`Unknown mode input [${mode}]. Must be one of: ${allowedModes.join(", ")}`
2627
);
2728
return;
2829
}
@@ -38,28 +39,35 @@ Toolkit.run(async (tools) => {
3839

3940
const appliedLabels = labels.map((label) => label.name);
4041

42+
// How many labels overlap?
4143
let intersection = allowedLabels.filter((x) => appliedLabels.includes(x));
4244

4345
if (mode === "exactly" && intersection.length !== count) {
4446
tools.exit.failure(
45-
`Label error. Requires exactly ${count} of: ${allowedLabels.join(", ")}`
47+
`Label error. Requires exactly ${count} of: ${allowedLabels.join(
48+
", "
49+
)}. Found: ${appliedLabels.join(", ")}`
4650
);
4751
return;
4852
}
4953

5054
if (mode === "minimum" && intersection.length < count) {
5155
tools.exit.failure(
52-
`Label error. Requires at least ${count} of: ${allowedLabels.join(", ")}`
56+
`Label error. Requires at least ${count} of: ${allowedLabels.join(
57+
", "
58+
)}. Found: ${appliedLabels.join(", ")}`
5359
);
5460
return;
5561
}
5662

5763
if (mode === "maximum" && intersection.length > count) {
5864
tools.exit.failure(
59-
`Label error. Requires at most ${count} of: ${allowedLabels.join(", ")}`
65+
`Label error. Requires at most ${count} of: ${allowedLabels.join(
66+
", "
67+
)}. Found: ${appliedLabels.join(", ")}`
6068
);
6169
return;
6270
}
6371

64-
tools.exit.success("Action complete");
72+
tools.exit.success("Complete");
6573
});

index.test.js

+246-17
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,252 @@
1-
const { Toolkit } = require('actions-toolkit')
1+
const { Toolkit } = require("actions-toolkit");
2+
const mockedEnv = require("mocked-env");
23

3-
describe('Required Labels', () => {
4-
let action, tools
4+
describe("Required Labels", () => {
5+
let action, tools;
56

67
// Mock Toolkit.run to define `action` so we can call it
7-
Toolkit.run = jest.fn((actionFn) => { action = actionFn })
8+
Toolkit.run = jest.fn((actionFn) => {
9+
action = actionFn;
10+
});
11+
812
// Load up our entrypoint file
9-
require('.')
13+
require(".");
1014

15+
let restore;
16+
let restoreTest;
1117
beforeEach(() => {
12-
// Create a new Toolkit instance
13-
tools = new Toolkit()
14-
// Mock methods on it!
15-
tools.exit.success = jest.fn()
16-
})
17-
18-
it('exits successfully', () => {
19-
action(tools)
20-
expect(tools.exit.success).toHaveBeenCalled()
21-
expect(tools.exit.success).toHaveBeenCalledWith('We did it!')
22-
})
23-
})
18+
restore = mockedEnv({
19+
GITHUB_WORKFLOW: "demo-workflow",
20+
GITHUB_ACTION: "required-labels",
21+
GITHUB_ACTOR: "mheap",
22+
GITHUB_REPOSITORY: "mheap/missing-repo",
23+
GITHUB_WORKSPACE: "/github/workspace",
24+
GITHUB_SHA: "e21490305ed7ac0897b7c7c54c88bb47f7a6d6c4",
25+
GITHUB_EVENT_NAME: "",
26+
GITHUB_EVENT_PATH: "",
27+
});
28+
29+
tools = new Toolkit();
30+
tools.context.loadPerTestEnv = function () {
31+
this.payload = process.env.GITHUB_EVENT_PATH
32+
? require(process.env.GITHUB_EVENT_PATH)
33+
: {};
34+
this.event = process.env.GITHUB_EVENT_NAME;
35+
};
36+
tools.exit.success = jest.fn();
37+
tools.exit.failure = jest.fn();
38+
});
39+
40+
afterEach(() => {
41+
restore();
42+
restoreTest();
43+
jest.resetModules();
44+
});
45+
46+
describe("success", () => {
47+
it("exact count", () => {
48+
// Create a new Toolkit instance
49+
restoreTest = mockPr(tools, ["enhancement"], {
50+
INPUT_LABELS: "enhancement,bug",
51+
INPUT_MODE: "exactly",
52+
INPUT_COUNT: "1",
53+
});
54+
55+
action(tools);
56+
expect(tools.exit.success).toBeCalledTimes(1);
57+
expect(tools.exit.success).toBeCalledWith("Complete");
58+
});
59+
60+
it("at least X", () => {
61+
// Create a new Toolkit instance
62+
restoreTest = mockPr(tools, ["enhancement", "triage"], {
63+
INPUT_LABELS: "enhancement,bug,triage",
64+
INPUT_MODE: "minimum",
65+
INPUT_COUNT: "2",
66+
});
67+
68+
action(tools);
69+
expect(tools.exit.success).toBeCalledTimes(1);
70+
expect(tools.exit.success).toBeCalledWith("Complete");
71+
});
72+
73+
it("at most X", () => {
74+
// Create a new Toolkit instance
75+
restoreTest = mockPr(tools, ["enhancement", "triage"], {
76+
INPUT_LABELS: "enhancement,bug,triage",
77+
INPUT_MODE: "maximum",
78+
INPUT_COUNT: "2",
79+
});
80+
81+
action(tools);
82+
expect(tools.exit.success).toBeCalledTimes(1);
83+
expect(tools.exit.success).toBeCalledWith("Complete");
84+
});
85+
});
86+
87+
describe("failure", () => {
88+
it("exact count", () => {
89+
// Create a new Toolkit instance
90+
restoreTest = mockPr(tools, ["enhancement", "bug"], {
91+
INPUT_LABELS: "enhancement,bug",
92+
INPUT_MODE: "exactly",
93+
INPUT_COUNT: "1",
94+
});
95+
96+
action(tools);
97+
expect(tools.exit.failure).toBeCalledTimes(1);
98+
expect(tools.exit.failure).toBeCalledWith(
99+
"Label error. Requires exactly 1 of: enhancement, bug. Found: enhancement, bug"
100+
);
101+
});
102+
103+
it("at least X", () => {
104+
// Create a new Toolkit instance
105+
restoreTest = mockPr(tools, ["enhancement"], {
106+
INPUT_LABELS: "enhancement,bug,triage",
107+
INPUT_MODE: "minimum",
108+
INPUT_COUNT: "2",
109+
});
110+
111+
action(tools);
112+
expect(tools.exit.failure).toBeCalledTimes(1);
113+
expect(tools.exit.failure).toBeCalledWith(
114+
"Label error. Requires at least 2 of: enhancement, bug, triage. Found: enhancement"
115+
);
116+
});
117+
118+
it("at most X", () => {
119+
// Create a new Toolkit instance
120+
restoreTest = mockPr(tools, ["enhancement", "triage", "bug"], {
121+
INPUT_LABELS: "enhancement,bug,triage",
122+
INPUT_MODE: "maximum",
123+
INPUT_COUNT: "2",
124+
});
125+
126+
action(tools);
127+
expect(tools.exit.failure).toBeCalledTimes(1);
128+
expect(tools.exit.failure).toBeCalledWith(
129+
"Label error. Requires at most 2 of: enhancement, bug, triage. Found: enhancement, triage, bug"
130+
);
131+
});
132+
});
133+
134+
describe("validation", () => {
135+
it("missing INPUT_COUNT", () => {
136+
restoreTest = mockPr(tools, [], {
137+
INPUT_LABELS: "enhancement,bug",
138+
INPUT_MODE: "exactly",
139+
});
140+
action(tools);
141+
expect(tools.exit.failure).toBeCalledTimes(1);
142+
expect(tools.exit.failure).toBeCalledWith(
143+
"[count] input is not provided"
144+
);
145+
});
146+
147+
it("missing INPUT_LABELS", () => {
148+
restoreTest = mockPr(tools, [], {
149+
INPUT_MODE: "exactly",
150+
INPUT_COUNT: "1",
151+
});
152+
153+
action(tools);
154+
expect(tools.exit.failure).toBeCalledTimes(1);
155+
expect(tools.exit.failure).toBeCalledWith(
156+
"[labels] input is empty or not provided"
157+
);
158+
});
159+
160+
it("unknown mode", () => {
161+
restoreTest = mockPr(tools, [], {
162+
INPUT_MODE: "bananas",
163+
INPUT_LABELS: "enhancement,bug",
164+
INPUT_COUNT: "1",
165+
});
166+
167+
action(tools);
168+
expect(tools.exit.failure).toBeCalledTimes(1);
169+
expect(tools.exit.failure).toBeCalledWith(
170+
"Unknown mode input [bananas]. Must be one of: exactly, minimum, maximum"
171+
);
172+
});
173+
});
174+
175+
describe("data integrity", () => {
176+
it("supports spaces in INPUT_LABELS", () => {
177+
restoreTest = mockPr(tools, ["enhancement"], {
178+
INPUT_LABELS: "enhancement , bug",
179+
INPUT_MODE: "exactly",
180+
INPUT_COUNT: "1",
181+
});
182+
183+
action(tools);
184+
expect(tools.exit.success).toBeCalledTimes(1);
185+
expect(tools.exit.success).toBeCalledWith("Complete");
186+
});
187+
188+
it("fetches labels from the API when provided with a GITHUB_TOKEN", async () => {
189+
tools.github.issues.listLabelsOnIssue = jest
190+
.fn()
191+
.mockReturnValue(Promise.resolve({ data: [{ name: "enhancement" }] }));
192+
193+
restoreTest = mockPr(tools, ["should_not_be_used"], {
194+
GITHUB_TOKEN: "this_is_a_test_token",
195+
INPUT_LABELS: "enhancement , bug",
196+
INPUT_MODE: "exactly",
197+
INPUT_COUNT: "1",
198+
});
199+
200+
await action(tools);
201+
202+
expect(tools.github.issues.listLabelsOnIssue).toBeCalledTimes(1);
203+
expect(tools.github.issues.listLabelsOnIssue).toBeCalledWith({
204+
issue_number: 28,
205+
owner: "mheap",
206+
repo: "missing-repo",
207+
});
208+
209+
expect(tools.exit.success).toBeCalledTimes(1);
210+
expect(tools.exit.success).toBeCalledWith("Complete");
211+
});
212+
});
213+
});
214+
215+
function mockPr(tools, labels, env) {
216+
return mockEvent(
217+
tools,
218+
"pull_request",
219+
{
220+
action: "opened",
221+
pull_request: {
222+
number: 28,
223+
labels: labels.map((name) => {
224+
return { name };
225+
}),
226+
},
227+
},
228+
env
229+
);
230+
}
231+
232+
function mockEvent(tools, eventName, mockPayload, additionalParams = {}) {
233+
jest.mock(
234+
"/github/workspace/event.json",
235+
() => {
236+
return mockPayload;
237+
},
238+
{
239+
virtual: true,
240+
}
241+
);
242+
243+
const params = {
244+
GITHUB_EVENT_NAME: eventName,
245+
GITHUB_EVENT_PATH: "/github/workspace/event.json",
246+
...additionalParams,
247+
};
248+
249+
const r = mockedEnv(params);
250+
tools.context.loadPerTestEnv();
251+
return r;
252+
}

0 commit comments

Comments
 (0)