diff --git a/__tests__/bundle/bundle.test.ts b/__tests__/bundle/bundle.test.ts index c633ac85..aa6a0126 100644 --- a/__tests__/bundle/bundle.test.ts +++ b/__tests__/bundle/bundle.test.ts @@ -13,7 +13,7 @@ import pullReqOpenedEvent from '../fixtures/pullReq/pullReqOpenedEvent.json' import pullReqReviewSubmittedEvent from '../fixtures/pullReq/pullReqReviewSubmittedEvent.json' import { blobSha, prCommentEvent, pullBody } from '../utils/ownersFixtures' import { start } from './fakeGithub' -import { comment, configReads, helpersFor, membershipReads, ownersProbe, ownersReads, queueRead, repo, token } from './helpers' +import { comment, configReads, helpersFor, membershipReads, ownersProbe, ownersReads, queueRead, repo, repoLabels, token } from './helpers' import { bundlePath, runBundle } from './runBundle' vi.setConfig({ testTimeout: 30_000 }) @@ -26,10 +26,6 @@ function openPr(labels: string[], overrides: Record = {}) { return { ...pr, labels: labels.map(name => ({ name })), ...overrides } } -function repoLabels(...names: string[]) { - return { status: 200, body: names.map(name => ({ name })) } -} - function yamlFile(text: string) { const file = structuredClone(labelFileContents) file.content = Buffer.from(text).toString('base64') diff --git a/__tests__/bundle/helpers.ts b/__tests__/bundle/helpers.ts index 78d61e38..9c88a553 100644 --- a/__tests__/bundle/helpers.ts +++ b/__tests__/bundle/helpers.ts @@ -60,6 +60,11 @@ export function membershipReads(login: string) { return [`GET /orgs/Codertocat/members/${login}`, `GET ${repo}/collaborators/${login}`] } +// the repository's label list, as the label commands read it before applying a label +export function repoLabels(...names: string[]) { + return { status: 200, body: names.map(name => ({ name })) } +} + // the helpers that route or inspect a suite's fake GitHub; the suite starts it in beforeAll, after binding these, // so `fake` is called on each use export function helpersFor(fake: () => FakeGithub) { diff --git a/__tests__/bundle/labelCommands.test.ts b/__tests__/bundle/labelCommands.test.ts new file mode 100644 index 00000000..70c42dde --- /dev/null +++ b/__tests__/bundle/labelCommands.test.ts @@ -0,0 +1,74 @@ +import type { FakeGithub } from './fakeGithub' +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +import labelFileContents from '../fixtures/labels/labelFileContentsResp.json' +import { start } from './fakeGithub' +import { comment, configReads, helpersFor, repo, repoLabels, token } from './helpers' +import { runBundle } from './runBundle' + +vi.setConfig({ testTimeout: 30_000 }) + +const labelsRead = `GET ${repo}/labels?per_page=100` +const issueRead = `GET ${repo}/issues/1` + +function issueLabels(...names: string[]) { + return { status: 200, body: { labels: names.map(name => ({ name })) } } +} + +function deleteRoute(label: string) { + return `DELETE ${repo}/issues/1/labels/${encodeURIComponent(label)}` +} + +// the label-command paths dist/index.js is not otherwise driven through: a fixed-label removal, a prefixed +// command served by Prow's built-in defaults, and the no-configuration failure docs/labeling.md promises +describe('dist/index.js fixed and built-in prefixed label commands', () => { + let gh: FakeGithub + const { expectCommandThenConfig, expectRequests } = helpersFor(() => gh) + + beforeAll(async () => { + gh = await start() + }) + afterEach(() => gh.reset()) + afterAll(() => gh.close()) + + function run(body: string, command: string) { + return runBundle({ eventName: 'issue_comment', payload: comment(body), inputs: { ...token, 'prow-commands': command }, apiUrl: gh.url }) + } + + it('/remove-help removes help wanted and good first issue, matching label case-insensitively', async () => { + gh.route('GET', `${repo}/issues/1`, issueLabels('Help Wanted', 'good first issue', 'kind/bug')) + gh.route('DELETE', `${repo}/issues/1/labels/Help%20Wanted`, { status: 200, body: [] }) + gh.route('DELETE', `${repo}/issues/1/labels/good%20first%20issue`, { status: 200, body: [] }) + + const result = await run('/remove-help', '/help') + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + expect(gh.requestsMatching('POST', /./)).toEqual([]) + expectCommandThenConfig([issueRead, deleteRoute('Help Wanted'), deleteRoute('good first issue')]) + }) + + // .prowlabels.yaml has no lifecycle key, so the command falls back to Prow's built-in values + it('/lifecycle stale from the built-in defaults is exclusive: removes lifecycle/frozen, then adds lifecycle/stale', async () => { + gh.route('GET', `${repo}/contents/.prowlabels.yaml`, { status: 200, body: labelFileContents }) + gh.route('GET', `${repo}/issues/1`, issueLabels('lifecycle/frozen', 'kind/bug')) + gh.route('DELETE', `${repo}/issues/1/labels/lifecycle%2Ffrozen`, { status: 200, body: [] }) + gh.route('GET', `${repo}/labels`, repoLabels('lifecycle/frozen', 'lifecycle/stale', 'lifecycle/rotten')) + gh.route('POST', `${repo}/issues/1/labels`, { status: 200, body: [] }) + + const result = await run('/lifecycle stale', '/lifecycle') + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + expect(gh.requestsMatching('POST', /\/issues\/1\/labels$/).map(r => r.body)).toEqual([{ labels: ['lifecycle/stale'] }]) + expectRequests(configReads({ repo: '.prowlabels.yaml' }), [issueRead, deleteRoute('lifecycle/frozen'), labelsRead, `POST ${repo}/issues/1/labels`]) + }) + + it('/lifecycle stale with no configuration file in any tier fails the run, as docs/labeling.md says it must', async () => { + const result = await run('/lifecycle stale', '/lifecycle') + + expect(result.status, result.stdout).toBe(1) + expect(result.errors.some(e => e.includes('no prow configuration found'))).toBe(true) + expectRequests(configReads(), []) + }) +})