diff --git a/dist/schema/output.d.ts b/dist/schema/output.d.ts index 9fe7fea..5eea8de 100644 --- a/dist/schema/output.d.ts +++ b/dist/schema/output.d.ts @@ -1,13 +1,4 @@ -import { z } from 'zod'; -declare const outputPolicySchema: z.ZodObject<{ - template: z.ZodString; - section: z.ZodRecord>; -}, "strip", z.ZodTypeAny, { +export type OutputPolicy = { template: string; section: Record; -}, { - template: string; - section: Record; -}>; -export type OutputPolicy = z.infer; -export {}; +}; diff --git a/dist/schema/output.js b/dist/schema/output.js index 0d2694b..e7fc75f 100644 --- a/dist/schema/output.js +++ b/dist/schema/output.js @@ -1,6 +1,2 @@ -import { z } from 'zod'; -const outputPolicySchema = z.object({ - template: z.string(), - section: z.record(z.array(z.string())), -}); +export {}; //# sourceMappingURL=output.js.map \ No newline at end of file diff --git a/dist/schema/output.js.map b/dist/schema/output.js.map index eaebaf4..e5a261f 100644 --- a/dist/schema/output.js.map +++ b/dist/schema/output.js.map @@ -1 +1 @@ -{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/schema/output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;CACvC,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/schema/output.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/src/schema/output.ts b/src/schema/output.ts index 03e3cb0..4cf3ff0 100644 --- a/src/schema/output.ts +++ b/src/schema/output.ts @@ -1,8 +1,4 @@ -import { z } from 'zod'; - -const outputPolicySchema = z.object({ - template: z.string(), - section: z.record(z.array(z.string())), -}); - -export type OutputPolicy = z.infer; +export type OutputPolicy = { + template: string; + section: Record; +}; diff --git a/test/action.test.ts b/test/action.test.ts index 79b529c..8026458 100644 --- a/test/action.test.ts +++ b/test/action.test.ts @@ -1,9 +1,375 @@ -import { describe, expect, test } from 'vitest'; +import { Octokit } from '@octokit/core'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import action from '../src/action'; +import { CustomOctokit } from '../src/octokit'; + +import { + basicConfig, + emptyConfig, + templateConfig, +} from './unit/config.fixture'; +import { basicForm, dropdownForm } from './unit/issue-form.fixture'; + +function setDefaultInputs() { + // Path to configuration file + vi.stubEnv('INPUT_CONFIG-PATH', '.github/advanced-issue-labeler.yml'); + // GitHub token used to set issue labels + vi.stubEnv('INPUT_TOKEN', 'mock-token'); +} + +const mocks = vi.hoisted(() => { + const octokitCore = { + '@octokit/core': { + request: vi.fn(), + config: { + get: vi.fn(), + }, + }, + }; + + const actionsCore = { + '@actions/core': { + error: vi.fn(), + setOutput: vi.fn(), + }, + }; + + return { + ...octokitCore, + ...actionsCore, + }; +}); + +// Mock @octokit/core module +vi.mock('@octokit/core', () => { + const Octokit = vi.fn(() => ({ + request: mocks['@octokit/core'].request, + config: { + get: mocks['@octokit/core'].config.get, + }, + })); + return { Octokit }; +}); + +// Mock @actions/core module +vi.mock('@actions/core', async () => { + const actual = await vi.importActual('@actions/core'); + return { + ...(actual as any), + error: mocks['@actions/core'].error, + setOutput: mocks['@actions/core'].setOutput, + }; +}); + +// Mock @actions/github module +vi.mock('@actions/github', () => { + vi.stubEnv( + 'GITHUB_REPOSITORY', + 'redhat-plumbers-in-action/advanced-issue-labeler' + ); + + return { + context: { + repo: { + owner: process.env['GITHUB_REPOSITORY']?.split('/')[0], + repo: process.env['GITHUB_REPOSITORY']?.split('/')[1], + }, + issue: { + number: 1, + }, + }, + }; +}); describe('Integration test', () => { - test('Smoke', async () => { - expect(action).toBeInstanceOf(Function); + beforeEach(() => { + // Mock Action environment + vi.stubEnv('RUNNER_DEBUG', '1'); + vi.stubEnv( + 'GITHUB_REPOSITORY', + 'redhat-plumbers-in-action/advanced-issue-labeler' + ); + + // Mock GitHub API + vi.mocked(mocks['@octokit/core'].request).mockImplementation(path => { + switch (path) { + case 'POST /repos/{owner}/{repo}/issues/{issue_number}/labels': + return { + status: 200, + data: {}, + }; + + default: + throw new Error(`Unexpected endpoint: ${path}`); + } + }); + + vi.mocked(mocks['@actions/core'].setOutput).mockImplementation( + (name, value) => { + vi.stubEnv(`OUTPUT_${name.toUpperCase()}`, value); + } + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + test('Input based labeling - basic', async () => { + setDefaultInputs(); + vi.stubEnv('INPUT_ISSUE-FORM', JSON.stringify(basicForm)); + vi.stubEnv('INPUT_SECTION', 'severity'); + + vi.mocked(mocks['@octokit/core']).config.get.mockImplementation( + async (params: { owner: string; repo: string; path: string }) => { + expect(params).toMatchInlineSnapshot(` + { + "owner": "redhat-plumbers-in-action", + "path": ".github/advanced-issue-labeler.yml", + "repo": "advanced-issue-labeler", + } + `); + return Promise.resolve({ config: emptyConfig }); + } + ); + + // Run action + const octokit = new Octokit({ auth: 'mock-token' }); + await action(octokit as CustomOctokit); + + // Check outputs + expect(process.env['OUTPUT_LABELS']).toMatchInlineSnapshot(`"["High"]"`); + expect(process.env['OUTPUT_POLICY']).toMatchInlineSnapshot( + `"{"template":"","section":{"severity":["High"]}}"` + ); + + // Check if the action has set the labels + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledTimes(1); + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/issues/{issue_number}/labels', + { + owner: 'redhat-plumbers-in-action', + repo: 'advanced-issue-labeler', + issue_number: 1, + labels: ['High'], + } + ); + }); + + test('Input based labeling - block-list', async () => { + setDefaultInputs(); + vi.stubEnv('INPUT_ISSUE-FORM', JSON.stringify(dropdownForm)); + vi.stubEnv('INPUT_SECTION', 'type'); + vi.stubEnv('INPUT_BLOCK-LIST', 'other\nnone'); + + vi.mocked(mocks['@octokit/core']).config.get.mockImplementation( + async (params: { owner: string; repo: string; path: string }) => { + expect(params).toMatchInlineSnapshot(` + { + "owner": "redhat-plumbers-in-action", + "path": ".github/advanced-issue-labeler.yml", + "repo": "advanced-issue-labeler", + } + `); + return Promise.resolve({ config: emptyConfig }); + } + ); + + // Run action + const octokit = new Octokit({ auth: 'mock-token' }); + await action(octokit as CustomOctokit); + + // Check outputs + expect(process.env['OUTPUT_LABELS']).toMatchInlineSnapshot( + `"["Bug Report","Feature Request"]"` + ); + expect(process.env['OUTPUT_POLICY']).toMatchInlineSnapshot( + `"{"template":"","section":{"type":["Bug Report","Feature Request"]}}"` + ); + + // Check if the action has set the labels + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledTimes(1); + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/issues/{issue_number}/labels', + { + owner: 'redhat-plumbers-in-action', + repo: 'advanced-issue-labeler', + issue_number: 1, + labels: ['Bug Report', 'Feature Request'], + } + ); + }); + + test('Config based labeling - default template', async () => { + setDefaultInputs(); + vi.stubEnv('INPUT_ISSUE-FORM', JSON.stringify(dropdownForm)); + + vi.mocked(mocks['@octokit/core']).config.get.mockImplementation( + async (params: { owner: string; repo: string; path: string }) => { + expect(params).toMatchInlineSnapshot(` + { + "owner": "redhat-plumbers-in-action", + "path": ".github/advanced-issue-labeler.yml", + "repo": "advanced-issue-labeler", + } + `); + return Promise.resolve({ config: basicConfig }); + } + ); + + // Run action + const octokit = new Octokit({ auth: 'mock-token' }); + await action(octokit as CustomOctokit); + + // Check outputs + expect(process.env['OUTPUT_LABELS']).toMatchInlineSnapshot( + `"["bug 🐛","RFE 🎁"]"` + ); + expect(process.env['OUTPUT_POLICY']).toMatchInlineSnapshot( + `"{"template":"","section":{"type":["bug 🐛","RFE 🎁"]}}"` + ); + + // Check if the action has set the labels + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledTimes(1); + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/issues/{issue_number}/labels', + { + owner: 'redhat-plumbers-in-action', + repo: 'advanced-issue-labeler', + issue_number: 1, + labels: ['bug 🐛', 'RFE 🎁'], + } + ); + }); + + test('Config based labeling - advanced example/multiple sections', async () => { + setDefaultInputs(); + vi.stubEnv('INPUT_ISSUE-FORM', JSON.stringify(dropdownForm)); + vi.stubEnv('INPUT_TEMPLATE', 'bug.yml'); + + vi.mocked(mocks['@octokit/core']).config.get.mockImplementation( + async (params: { owner: string; repo: string; path: string }) => { + expect(params).toMatchInlineSnapshot(` + { + "owner": "redhat-plumbers-in-action", + "path": ".github/advanced-issue-labeler.yml", + "repo": "advanced-issue-labeler", + } + `); + return Promise.resolve({ config: templateConfig }); + } + ); + + // Run action + const octokit = new Octokit({ auth: 'mock-token' }); + await action(octokit as CustomOctokit); + + // Check outputs + expect(process.env['OUTPUT_LABELS']).toMatchInlineSnapshot( + `"["bug 🐛","RFE 🎁","high"]"` + ); + expect(process.env['OUTPUT_POLICY']).toMatchInlineSnapshot( + `"{"template":"bug.yml","section":{"type":["bug 🐛","RFE 🎁"],"severity":["high"]}}"` + ); + + // Check if the action has set the labels + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledTimes(1); + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/issues/{issue_number}/labels', + { + owner: 'redhat-plumbers-in-action', + repo: 'advanced-issue-labeler', + issue_number: 1, + labels: ['bug 🐛', 'RFE 🎁', 'high'], + } + ); + }); + + test('No labels to set', async () => { + setDefaultInputs(); + vi.stubEnv('INPUT_ISSUE-FORM', JSON.stringify(basicForm)); + vi.stubEnv('INPUT_SECTION', 'nonexistent'); + + vi.mocked(mocks['@octokit/core']).config.get.mockImplementation( + async (params: { owner: string; repo: string; path: string }) => { + expect(params).toMatchInlineSnapshot(` + { + "owner": "redhat-plumbers-in-action", + "path": ".github/advanced-issue-labeler.yml", + "repo": "advanced-issue-labeler", + } + `); + return Promise.resolve({ config: emptyConfig }); + } + ); + + // Run action + const octokit = new Octokit({ auth: 'mock-token' }); + await action(octokit as CustomOctokit); + + // Check outputs + expect(process.env['OUTPUT_LABELS']).toMatchInlineSnapshot(`"[]"`); + expect(process.env['OUTPUT_POLICY']).toMatchInlineSnapshot( + `"{"template":"","section":{}}"` + ); + + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledTimes(0); + }); + + test('Wrong issue form format', async () => { + setDefaultInputs(); + vi.stubEnv('INPUT_ISSUE-FORM', 'wrong'); + vi.stubEnv('INPUT_SECTION', 'nonexistent'); + + vi.mocked(mocks['@octokit/core']).config.get.mockImplementation( + async (params: { owner: string; repo: string; path: string }) => { + expect(params).toMatchInlineSnapshot(` + { + "owner": "redhat-plumbers-in-action", + "path": ".github/advanced-issue-labeler.yml", + "repo": "advanced-issue-labeler", + } + `); + return Promise.resolve({ config: emptyConfig }); + } + ); + + // Run action + const octokit = new Octokit({ auth: 'mock-token' }); + try { + await action(octokit as CustomOctokit); + } catch (error) { + expect(error.message).toMatchInlineSnapshot( + `"Unexpected token 'w', "wrong" is not valid JSON"` + ); + } + + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledTimes(0); + + // Test wrong JSON format + vi.stubEnv('INPUT_ISSUE-FORM', JSON.stringify('wrong')); + + // Run action + try { + await action(octokit as CustomOctokit); + } catch (error) { + expect(error.message).toMatchInlineSnapshot( + ` + "Incorrect format of provided 'issue-form' input: [ + { + "code": "invalid_type", + "expected": "object", + "received": "string", + "path": [], + "message": "Expected object, received string" + } + ]" + ` + ); + } + + expect(vi.mocked(mocks['@octokit/core'].request)).toHaveBeenCalledTimes(0); }); }); diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts index e56bfe2..1973da2 100644 --- a/test/unit/config.test.ts +++ b/test/unit/config.test.ts @@ -222,10 +222,12 @@ describe('Test Config class', () => { }); test('error when policy is empty', () => { - const config = new Config(emptyConfig, '.github/issue-labeler.yml'); + const config = new Config(emptyConfig, 'src/custom-labeler.yml'); - expect(() => config.getTemplatePolicy(undefined)).toThrowError( - `Missing configuration. Please setup 'Advanced Issue Labeler' Action using '.github/issue-labeler.yml' file.` + expect(() => + config.getTemplatePolicy(undefined) + ).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing configuration. Please setup 'Advanced Issue Labeler' Action using 'src/custom-labeler.yml' file.]` ); }); }); diff --git a/test/unit/issue-form.fixture.ts b/test/unit/issue-form.fixture.ts index 38b9ad7..aa1d1f5 100644 --- a/test/unit/issue-form.fixture.ts +++ b/test/unit/issue-form.fixture.ts @@ -3,6 +3,7 @@ export const emptyForm = {}; export const basicForm = { title: 'Test issue', body: 'Test body', + severity: 'High', }; export const dropdownForm = { diff --git a/test/unit/issue-form.test.ts b/test/unit/issue-form.test.ts index 0239d69..c470733 100644 --- a/test/unit/issue-form.test.ts +++ b/test/unit/issue-form.test.ts @@ -10,6 +10,7 @@ describe('Test IssueForm class', () => { expect(issueForm.parsed).toMatchInlineSnapshot(` { "body": "Test body", + "severity": "High", "title": "Test issue", } `);