Skip to content

Commit

Permalink
Add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
paescuj committed Aug 12, 2024
1 parent 45532ea commit 7fbef3e
Show file tree
Hide file tree
Showing 11 changed files with 932 additions and 294 deletions.
18 changes: 16 additions & 2 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,22 @@ jobs:
- name: Run typecheck
run: pnpm run typecheck

test:
name: Test
test-unit:
name: Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup env
uses: ./.github/actions/setup

- name: Run tests
run: pnpm test

test-action:
name: Action Test
runs-on: ubuntu-latest
permissions:
issues: read
Expand Down
42 changes: 21 additions & 21 deletions dist/index.js

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"type": "module",
"packageManager": "pnpm@9.6.0",
"packageManager": "pnpm@9.7.0",
"engines": {
"node": "20",
"pnpm": "9"
Expand All @@ -9,18 +9,19 @@
"build": "pnpm run '/^(bundle|typecheck)$/'",
"bundle": "tsup",
"typecheck": "tsc --noEmit",
"lint": "eslint ."
"lint": "eslint .",
"test": "vitest --watch=false"
},
"devDependencies": {
"@actions/core": "1.10.1",
"@actions/github": "6.0.0",
"@directus/eslint-config": "link:../eslint-config",
"@directus/tsconfig": "1.0.1",
"@directus/tsconfig": "2.0.0",
"@octokit/graphql": "8.1.1",
"dayjs": "1.11.12",
"eslint": "9.8.0",
"eslint-plugin-format": "0.1.2",
"eslint": "9.9.0",
"tsup": "8.2.4",
"typescript": "5.5.4"
"typescript": "5.5.4",
"vitest": "2.0.5"
}
}
711 changes: 471 additions & 240 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

25 changes: 2 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,3 @@
import core from '@actions/core';
import { main } from './main.js';

import { getStaleIssues } from './lib/get-stale-issues.js';
import { closeIssue } from './lib/close-issue.js';
import { getConfig } from './lib/get-config.js';

try {
const config = await getConfig();

const issues = getStaleIssues(config);

const closedIssues = [];

for await (const issue of issues) {
await closeIssue(config, issue);
closedIssues.push(issue.number);
}

core.setOutput('closed-issues', closedIssues);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
core.setFailed(message);
}
await main();
65 changes: 65 additions & 0 deletions src/lib/close-issues.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { afterEach, expect, it, vi } from 'vitest';
import core from '@actions/core';

import type { Config } from './get-config.js';
import type { Issue } from './get-stale-issues.js';
import { closeIssue } from './close-issue.js';

const mockOctokit = {
rest: {
issues: {
createComment: vi.fn(),
update: vi.fn(),
},
},
} as unknown as Config['octokit'];

const mockConfig = {
octokit: mockOctokit,
ownerRepo: { owner: 'directus', repo: 'stale-issues-action' },
closeMessage: 'Closing this issue as it has become stale.',
} as Config;

const mockIssue = {
number: 1,
url: 'https://github.com/directus/stale-issues-action/issues/1',
staleSince: '1 day',
} as Issue;

const infoSpy = vi.spyOn(core, 'info').mockImplementation(() => {});

afterEach(() => {
vi.clearAllMocks();
});

it('creates a comment and closes the issue', async () => {
await closeIssue(mockConfig, mockIssue);

expect(mockOctokit.rest.issues.createComment).toBeCalledWith({
...mockConfig.ownerRepo,
issue_number: mockIssue.number,
body: mockConfig.closeMessage,
});

expect(mockOctokit.rest.issues.update).toBeCalledWith({
...mockConfig.ownerRepo,
issue_number: mockIssue.number,
state: 'closed',
state_reason: 'not_planned',
});

expect(infoSpy).toBeCalledWith(
`Issue #${mockIssue.number} (${mockIssue.url}) is stale since ${mockIssue.staleSince} and has been closed`,
);
});

it('prints info only in dry-run mode', async () => {
await closeIssue({ ...mockConfig, dryRun: true }, mockIssue);

expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled();
expect(mockOctokit.rest.issues.update).not.toHaveBeenCalled();

expect(infoSpy).toBeCalledWith(
`Issue #${mockIssue.number} (${mockIssue.url}) is stale since ${mockIssue.staleSince} and would have been closed (dry-run)`,
);
});
73 changes: 73 additions & 0 deletions src/lib/get-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import core from '@actions/core';
import github from '@actions/github';
import { beforeEach, expect, it, vi } from 'vitest';

import { type Config, getConfig } from './get-config.js';

vi.mock('@actions/github');

let actionInputs: Record<string, string> = {};

function getInput(name: string) {
return actionInputs[name] || '';
}

vi.spyOn(core, 'getInput').mockImplementation(getInput);
vi.spyOn(core, 'getBooleanInput').mockImplementation((name) => Boolean(getInput(name)));

const mockOctokit = { request: vi.fn() };

vi.spyOn(github, 'getOctokit').mockReturnValue(mockOctokit as unknown as Config['octokit']);

beforeEach(() => {
actionInputs = {
'github-token': 'token',
'github-repo': 'directus/stale-issues-action',
'stale-label': 'stale',
'days-before-close': '7',
'close-message': 'Closing this issue as it has become stale.',
};
});

it('returns the config if all inputs are valid', async () => {
await expect(getConfig()).resolves.toStrictEqual({
closeMessage: 'Closing this issue as it has become stale.',
daysBeforeClose: 7,
dryRun: false,
octokit: mockOctokit,
ownerRepo: {
owner: 'directus',
repo: 'stale-issues-action',
},
staleLabel: 'stale',

});
});

it('throws if the format of "github-repo" is invalid', async () => {
actionInputs['github-repo'] = 'invalid';

await expect(getConfig()).rejects.toThrow('"github-repo" must be provided in form of "<owner>/<repo>"');
});

it('throws if the given "stale-label" could not be found', async () => {
mockOctokit.request.mockRejectedValueOnce(new Error('label not found'));

await expect(getConfig()).rejects.toThrow(`"stale-label" doesn't refer to an existing label or repository cannot be accessed`);
});

it('warns if the given "stale-label" could not be found in dry-run mode', async () => {
actionInputs['dry-run'] = 'true';

const warnSpy = vi.spyOn(core, 'warning').mockImplementation(() => {});
mockOctokit.request.mockRejectedValueOnce(new Error('label not found'));

await expect(getConfig()).resolves.toBeDefined();
expect(warnSpy).toBeCalledWith(`"stale-label" doesn't refer to an existing label or repository cannot be accessed`);
});

it('throws if "days-before-close" is not a number', async () => {
actionInputs['days-before-close'] = 'invalid';

await expect(getConfig()).rejects.toThrow('"days-before-close" must be a number');
});
Loading

0 comments on commit 7fbef3e

Please sign in to comment.