Skip to content

Commit

Permalink
Add unit tests (#24)
Browse files Browse the repository at this point in the history
* Add unit tests

* Add GitHub Actions CI workflow

* Add Prettier
  • Loading branch information
mheap committed Jun 21, 2022
1 parent 3bdc29c commit c832d6e
Show file tree
Hide file tree
Showing 5 changed files with 8,417 additions and 41 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
@@ -0,0 +1,23 @@
name: Node.js CI

on: [push]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [14.x, 16.x, 18.x]

env:
CI: true
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- run: npm run lint
28 changes: 18 additions & 10 deletions index.js
@@ -1,28 +1,29 @@
const { Toolkit } = require("actions-toolkit");

Toolkit.run(async (tools) => {
// Process inputs for use later
const mode = tools.inputs.mode;
const count = parseInt(tools.inputs.count, 10);
const allowedLabels = tools.inputs.labels
.split(",")
.map((l) => l.trim())
.filter((r) => r);

if (allowedLabels.length === 0) {
tools.exit.failure("Missing input 'labels'");
// Validate inputs
if (tools.inputs.count === "") {
tools.exit.failure(`[count] input is not provided`);
return;
}

// Validation
if (count === "") {
tools.exit.failure(`Missing input.count`);
if (allowedLabels.length === 0) {
tools.exit.failure("[labels] input is empty or not provided");
return;
}

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

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

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

if (mode === "exactly" && intersection.length !== count) {
tools.exit.failure(
`Label error. Requires exactly ${count} of: ${allowedLabels.join(", ")}`
`Label error. Requires exactly ${count} of: ${allowedLabels.join(
", "
)}. Found: ${appliedLabels.join(", ")}`
);
return;
}

if (mode === "minimum" && intersection.length < count) {
tools.exit.failure(
`Label error. Requires at least ${count} of: ${allowedLabels.join(", ")}`
`Label error. Requires at least ${count} of: ${allowedLabels.join(
", "
)}. Found: ${appliedLabels.join(", ")}`
);
return;
}

if (mode === "maximum" && intersection.length > count) {
tools.exit.failure(
`Label error. Requires at most ${count} of: ${allowedLabels.join(", ")}`
`Label error. Requires at most ${count} of: ${allowedLabels.join(
", "
)}. Found: ${appliedLabels.join(", ")}`
);
return;
}

tools.exit.success("Action complete");
tools.exit.success("Complete");
});
263 changes: 246 additions & 17 deletions index.test.js
@@ -1,23 +1,252 @@
const { Toolkit } = require('actions-toolkit')
const { Toolkit } = require("actions-toolkit");
const mockedEnv = require("mocked-env");

describe('Required Labels', () => {
let action, tools
describe("Required Labels", () => {
let action, tools;

// Mock Toolkit.run to define `action` so we can call it
Toolkit.run = jest.fn((actionFn) => { action = actionFn })
Toolkit.run = jest.fn((actionFn) => {
action = actionFn;
});

// Load up our entrypoint file
require('.')
require(".");

let restore;
let restoreTest;
beforeEach(() => {
// Create a new Toolkit instance
tools = new Toolkit()
// Mock methods on it!
tools.exit.success = jest.fn()
})

it('exits successfully', () => {
action(tools)
expect(tools.exit.success).toHaveBeenCalled()
expect(tools.exit.success).toHaveBeenCalledWith('We did it!')
})
})
restore = mockedEnv({
GITHUB_WORKFLOW: "demo-workflow",
GITHUB_ACTION: "required-labels",
GITHUB_ACTOR: "mheap",
GITHUB_REPOSITORY: "mheap/missing-repo",
GITHUB_WORKSPACE: "/github/workspace",
GITHUB_SHA: "e21490305ed7ac0897b7c7c54c88bb47f7a6d6c4",
GITHUB_EVENT_NAME: "",
GITHUB_EVENT_PATH: "",
});

tools = new Toolkit();
tools.context.loadPerTestEnv = function () {
this.payload = process.env.GITHUB_EVENT_PATH
? require(process.env.GITHUB_EVENT_PATH)
: {};
this.event = process.env.GITHUB_EVENT_NAME;
};
tools.exit.success = jest.fn();
tools.exit.failure = jest.fn();
});

afterEach(() => {
restore();
restoreTest();
jest.resetModules();
});

describe("success", () => {
it("exact count", () => {
// Create a new Toolkit instance
restoreTest = mockPr(tools, ["enhancement"], {
INPUT_LABELS: "enhancement,bug",
INPUT_MODE: "exactly",
INPUT_COUNT: "1",
});

action(tools);
expect(tools.exit.success).toBeCalledTimes(1);
expect(tools.exit.success).toBeCalledWith("Complete");
});

it("at least X", () => {
// Create a new Toolkit instance
restoreTest = mockPr(tools, ["enhancement", "triage"], {
INPUT_LABELS: "enhancement,bug,triage",
INPUT_MODE: "minimum",
INPUT_COUNT: "2",
});

action(tools);
expect(tools.exit.success).toBeCalledTimes(1);
expect(tools.exit.success).toBeCalledWith("Complete");
});

it("at most X", () => {
// Create a new Toolkit instance
restoreTest = mockPr(tools, ["enhancement", "triage"], {
INPUT_LABELS: "enhancement,bug,triage",
INPUT_MODE: "maximum",
INPUT_COUNT: "2",
});

action(tools);
expect(tools.exit.success).toBeCalledTimes(1);
expect(tools.exit.success).toBeCalledWith("Complete");
});
});

describe("failure", () => {
it("exact count", () => {
// Create a new Toolkit instance
restoreTest = mockPr(tools, ["enhancement", "bug"], {
INPUT_LABELS: "enhancement,bug",
INPUT_MODE: "exactly",
INPUT_COUNT: "1",
});

action(tools);
expect(tools.exit.failure).toBeCalledTimes(1);
expect(tools.exit.failure).toBeCalledWith(
"Label error. Requires exactly 1 of: enhancement, bug. Found: enhancement, bug"
);
});

it("at least X", () => {
// Create a new Toolkit instance
restoreTest = mockPr(tools, ["enhancement"], {
INPUT_LABELS: "enhancement,bug,triage",
INPUT_MODE: "minimum",
INPUT_COUNT: "2",
});

action(tools);
expect(tools.exit.failure).toBeCalledTimes(1);
expect(tools.exit.failure).toBeCalledWith(
"Label error. Requires at least 2 of: enhancement, bug, triage. Found: enhancement"
);
});

it("at most X", () => {
// Create a new Toolkit instance
restoreTest = mockPr(tools, ["enhancement", "triage", "bug"], {
INPUT_LABELS: "enhancement,bug,triage",
INPUT_MODE: "maximum",
INPUT_COUNT: "2",
});

action(tools);
expect(tools.exit.failure).toBeCalledTimes(1);
expect(tools.exit.failure).toBeCalledWith(
"Label error. Requires at most 2 of: enhancement, bug, triage. Found: enhancement, triage, bug"
);
});
});

describe("validation", () => {
it("missing INPUT_COUNT", () => {
restoreTest = mockPr(tools, [], {
INPUT_LABELS: "enhancement,bug",
INPUT_MODE: "exactly",
});
action(tools);
expect(tools.exit.failure).toBeCalledTimes(1);
expect(tools.exit.failure).toBeCalledWith(
"[count] input is not provided"
);
});

it("missing INPUT_LABELS", () => {
restoreTest = mockPr(tools, [], {
INPUT_MODE: "exactly",
INPUT_COUNT: "1",
});

action(tools);
expect(tools.exit.failure).toBeCalledTimes(1);
expect(tools.exit.failure).toBeCalledWith(
"[labels] input is empty or not provided"
);
});

it("unknown mode", () => {
restoreTest = mockPr(tools, [], {
INPUT_MODE: "bananas",
INPUT_LABELS: "enhancement,bug",
INPUT_COUNT: "1",
});

action(tools);
expect(tools.exit.failure).toBeCalledTimes(1);
expect(tools.exit.failure).toBeCalledWith(
"Unknown mode input [bananas]. Must be one of: exactly, minimum, maximum"
);
});
});

describe("data integrity", () => {
it("supports spaces in INPUT_LABELS", () => {
restoreTest = mockPr(tools, ["enhancement"], {
INPUT_LABELS: "enhancement , bug",
INPUT_MODE: "exactly",
INPUT_COUNT: "1",
});

action(tools);
expect(tools.exit.success).toBeCalledTimes(1);
expect(tools.exit.success).toBeCalledWith("Complete");
});

it("fetches labels from the API when provided with a GITHUB_TOKEN", async () => {
tools.github.issues.listLabelsOnIssue = jest
.fn()
.mockReturnValue(Promise.resolve({ data: [{ name: "enhancement" }] }));

restoreTest = mockPr(tools, ["should_not_be_used"], {
GITHUB_TOKEN: "this_is_a_test_token",
INPUT_LABELS: "enhancement , bug",
INPUT_MODE: "exactly",
INPUT_COUNT: "1",
});

await action(tools);

expect(tools.github.issues.listLabelsOnIssue).toBeCalledTimes(1);
expect(tools.github.issues.listLabelsOnIssue).toBeCalledWith({
issue_number: 28,
owner: "mheap",
repo: "missing-repo",
});

expect(tools.exit.success).toBeCalledTimes(1);
expect(tools.exit.success).toBeCalledWith("Complete");
});
});
});

function mockPr(tools, labels, env) {
return mockEvent(
tools,
"pull_request",
{
action: "opened",
pull_request: {
number: 28,
labels: labels.map((name) => {
return { name };
}),
},
},
env
);
}

function mockEvent(tools, eventName, mockPayload, additionalParams = {}) {
jest.mock(
"/github/workspace/event.json",
() => {
return mockPayload;
},
{
virtual: true,
}
);

const params = {
GITHUB_EVENT_NAME: eventName,
GITHUB_EVENT_PATH: "/github/workspace/event.json",
...additionalParams,
};

const r = mockedEnv(params);
tools.context.loadPerTestEnv();
return r;
}

0 comments on commit c832d6e

Please sign in to comment.