import * as core from '@actions/core' import * as github from '@actions/github' import { WebhookPayload } from '@actions/github/lib/interfaces' import { rest } from 'msw' import { setupServer } from 'msw/node' import * as path from 'node:path' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import run from '../src/main' import apiResponse from './sample-pulls-api-response.json' const repoToken = '12345' const commitSha = 'abc123' const simpleMessage = 'hello world' type Inputs = { message: string | undefined 'message-path': string | undefined 'repo-owner': string 'repo-name': string 'repo-token': string 'message-id': string 'allow-repeats': string 'message-success'?: string 'message-failure'?: string 'message-cancelled'?: string 'message-skipped'?: string 'update-only'?: string status?: 'success' | 'failure' | 'cancelled' | 'skipped' } const defaultInputs: Inputs = { message: '', 'message-path': undefined, 'repo-owner': 'foo', 'repo-name': 'bar', 'repo-token': repoToken, 'message-id': 'add-pr-comment', 'allow-repeats': 'false', } const defaultIssueNumber = 1 let inputs = defaultInputs let issueNumber = defaultIssueNumber let getCommitPullsResponse let getIssueCommentsResponse let postIssueCommentsResponse = { id: 42, } type MessagePayload = { comment_id?: number body: string } let messagePayload: MessagePayload | undefined vi.mock('@actions/core') const handlers = [ rest.post( `https://api.github.com/repos/:repoUser/:repoName/issues/:issueNumber/comments`, async (req, res, ctx) => { messagePayload = await req.json() return res(ctx.status(200), ctx.json(postIssueCommentsResponse)) }, ), rest.patch( `https://api.github.com/repos/:repoUser/:repoName/issues/comments/:commentId`, async (req, res, ctx) => { messagePayload = await req.json() return res(ctx.status(200), ctx.json(postIssueCommentsResponse)) }, ), rest.get( `https://api.github.com/repos/:repoUser/:repoName/issues/:issueNumber/comments`, (req, res, ctx) => { return res(ctx.status(200), ctx.json(getIssueCommentsResponse)) }, ), rest.get( `https://api.github.com/repos/:repoUser/:repoName/commits/:commitSha/pulls`, (req, res, ctx) => { return res(ctx.status(200), ctx.json(getCommitPullsResponse)) }, ), ] const server = setupServer(...handlers) describe('add-pr-comment action', () => { beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) afterAll(() => server.close()) beforeEach(() => { inputs = { ...defaultInputs } issueNumber = defaultIssueNumber vi.resetModules() github.context.sha = commitSha // https://developer.github.com/webhooks/event-payloads/#issues github.context.payload = { pull_request: { number: issueNumber, }, repository: { full_name: `${inputs['repo-owner']}/${inputs['repo-name']}`, name: 'bar', owner: { login: 'bar', }, }, } as WebhookPayload }) afterEach(() => { vi.clearAllMocks() server.resetHandlers() }) vi.mocked(core.getInput).mockImplementation((name: string, options?: core.InputOptions) => { const value = inputs[name] ?? '' if (options?.required && value === undefined) { throw new Error(`${name} is required`) } return value }) it('creates a comment with message text', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'true' await expect(run()).resolves.not.toThrow() expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true') expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id) }) it('creates a comment with a message-path', async () => { inputs.message = undefined inputs['message-path'] = path.resolve(__dirname, './message.txt') inputs['allow-repeats'] = 'true' await expect(run()).resolves.not.toThrow() expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true') expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id) }) it('fails when both message and message-path are defined', async () => { inputs.message = 'foobar' inputs['message-path'] = path.resolve(__dirname, './message.txt') await expect(run()).resolves.not.toThrow() expect(core.setFailed).toHaveBeenCalledWith('must specify only one, message or message-path') }) it('creates a comment in an existing PR', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'true' github.context.payload = { ...github.context.payload, pull_request: { number: 0, }, } as WebhookPayload issueNumber = apiResponse.result[0].number getCommitPullsResponse = apiResponse.result await expect(run()).resolves.not.toThrow() expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true') }) it('does not create a comment when updateOnly is true and no existing comment is found', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'true' inputs['update-only'] = 'true' await expect(run()).resolves.not.toThrow() expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'false') }) it('creates a comment in another repo', async () => { inputs.message = simpleMessage inputs['repo-owner'] = 'my-owner' inputs['repo-name'] = 'my-repo' inputs['allow-repeats'] = 'true' github.context.payload = { ...github.context.payload, pull_request: { number: 0, }, } as WebhookPayload issueNumber = apiResponse.result[0].number getCommitPullsResponse = apiResponse.result await expect(run()).resolves.not.toThrow() expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true') expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id) }) it('safely exits when no issue can be found [using GITHUB_TOKEN in env]', async () => { process.env['GITHUB_TOKEN'] = repoToken inputs.message = simpleMessage inputs['allow-repeats'] = 'true' github.context.payload = { ...github.context.payload, pull_request: { number: 0, }, } as WebhookPayload getCommitPullsResponse = [] await run() expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'false') }) it('creates a message when the message id does not exist', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'false' inputs['message-id'] = 'custom-id' const replyBody = [ { body: `\n\n${simpleMessage}`, }, ] getIssueCommentsResponse = replyBody await run() expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true') }) it('identifies an existing message by id and updates it', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'false' const commentId = 123 const replyBody = [ { id: commentId, body: `\n\n${simpleMessage}`, }, ] getIssueCommentsResponse = replyBody postIssueCommentsResponse = { id: commentId, } await run() expect(core.setOutput).toHaveBeenCalledWith('comment-updated', 'true') expect(core.setOutput).toHaveBeenCalledWith('comment-id', commentId) }) it('overrides the default message with a success message on success', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'false' inputs['message-success'] = '666' inputs.status = 'success' const commentId = 123 getIssueCommentsResponse = [ { id: commentId, }, ] postIssueCommentsResponse = { id: commentId, } await run() expect(messagePayload?.body).toContain('666') }) it('overrides the default message with a failure message on failure', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'false' inputs['message-failure'] = '666' inputs.status = 'failure' const commentId = 123 getIssueCommentsResponse = [ { id: commentId, }, ] postIssueCommentsResponse = { id: commentId, } await run() expect(messagePayload?.body).toContain('666') }) it('overrides the default message with a cancelled message on cancelled', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'false' inputs['message-cancelled'] = '666' inputs.status = 'cancelled' const commentId = 123 getIssueCommentsResponse = [ { id: commentId, }, ] postIssueCommentsResponse = { id: commentId, } await run() expect(messagePayload?.body).toContain('666') }) it('overrides the default message with a skipped message on skipped', async () => { inputs.message = simpleMessage inputs['allow-repeats'] = 'false' inputs['message-skipped'] = '666' inputs.status = 'skipped' const commentId = 123 getIssueCommentsResponse = [ { id: commentId, }, ] postIssueCommentsResponse = { id: commentId, } await run() expect(messagePayload?.body).toContain('666') }) })