Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests #24

Merged
merged 3 commits into from Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}