Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
cache: npm
- run: npm ci
- run: npm run build
- run: npm run typecheck
- run: npm run lint
- run: npm run test:coverage
- name: Coverage summary
Expand Down
5 changes: 3 additions & 2 deletions __tests__/adapters/app/webhook.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { staticTokens } from './testApp'

import { describe, expect, it } from 'vitest'
import { receiveWebhook } from '../../../src/adapters/app/webhook'
import issueCommentEvent from '../../fixtures/issues/issueCommentEvent.json'
import { receiver, signedRequest, withInstallation } from './testApp'
Expand All @@ -18,7 +19,7 @@ describe('receiveWebhook', () => {
expect(r.dispatched).toHaveLength(1)
expect(r.dispatched[0]).toMatchObject({ eventName: 'issue_comment', repo: { owner: 'Codertocat', repo: 'Hello-World' } })
expect(r.dispatched[0]!.payload.installation).toEqual({ id: 7 })
expect((r.tokens as { requested: number[] }).requested).toEqual([7])
expect((r.tokens as ReturnType<typeof staticTokens>).requested).toEqual([7])
})

it('rejects a bad signature with 401 and dispatches nothing', async () => {
Expand Down
5 changes: 3 additions & 2 deletions __tests__/bundle/fakeGithub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export async function start(): Promise<FakeGithub> {
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address() as AddressInfo

return {
const github: FakeGithub = {
url: `http://127.0.0.1:${port}`,
requests,
unmatched,
Expand All @@ -154,7 +154,7 @@ export async function start(): Promise<FakeGithub> {
return unmatched.filter(r => !allowances.some(a => matches(a, r)))
},
assertNoUnexpectedRequests() {
const unexpected = this.unexpectedRequests()
const unexpected = github.unexpectedRequests()
if (unexpected.length > 0) {
const listing = unexpected.map(r => ` ${r.method} ${r.path}`).join('\n')
throw new Error(`the fake GitHub API answered ${unexpected.length} request(s) no route or allowance covers:\n${listing}`)
Expand Down Expand Up @@ -197,6 +197,7 @@ export async function start(): Promise<FakeGithub> {
})
},
}
return github
}

function mergeQueueRoute(queue: MergeQueueFixture): Route {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/bundle/sweepArms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('dist/index.js schedule sweep job arms', () => {
// requested_reviewers is optional on the pulls listing item (sweep.ts:153 `pr.requested_reviewers ?? []`): a fresh
// pull request listed without it has no requested reviewers, so blunderbuss reads the one review and asks for two
it('a fresh pull request listed without requested_reviewers: the one review is read and two reviewers are requested', async () => {
const pr = forkPr()
const pr: Partial<ReturnType<typeof forkPr>> = forkPr()
delete pr.requested_reviewers
routeSweep(pr)

Expand Down
10 changes: 5 additions & 5 deletions __tests__/configurationDocs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ const example = parseProwConfig(docPath, yamlBlock('`prow.yaml`'))

/** what the code resolves for a section nobody configured, keyed like the document headings */
const resolvedDefaults: Record<string, Record<string, unknown>> = {
tide: resolveTide({}, ''),
tide: { ...resolveTide({}, '') },
hold: { label: resolveHoldLabel({}) },
blunderbuss: blunderbussSettings(empty),
approve: approveSettings(empty),
lgtm: lgtmSettings(empty),
blunderbuss: { ...blunderbussSettings(empty) },
approve: { ...approveSettings(empty) },
lgtm: { ...lgtmSettings(empty) },
sweep: { lookback: defaultSweepLookback },
authorization: { ...resolveAuthorization({}) },
}
Expand All @@ -89,7 +89,7 @@ describe(`${docPath} prow.yaml example`, () => {
})

it.each(['tide', 'hold', 'approve', 'lgtm', 'sweep', 'authorization'])('spells out the code defaults of %s, as its comment claims', (key) => {
expect(example[key as keyof typeof resolvedDefaults]).toEqual(resolvedDefaults[key])
expect(example[key as keyof ProwConfig]).toEqual(resolvedDefaults[key])
})

it('names the tide.labels default of a repository with OWNERS files', () => {
Expand Down
5 changes: 3 additions & 2 deletions __tests__/cronJobTest/labelSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,8 @@ describe('label-sync job', () => {
it('fails the run when the configuration cannot be loaded', async () => {
server.use(
http.get(`${repo}/contents/.github%2Fprow.yaml`, utils.mockResponse(500, { message: 'boom' })),
...utils.noOrgOrRepoConfigExcept('.github/prow.yaml'),
// end the organization tier on its first probe: its chain would otherwise outlive this test (see configFoundAt)
...utils.configFoundAt('Codertocat/.project:prow.yaml', '.github/prow.yaml'),
)
const writes = recordWrites([])
const setFailed = vi.spyOn(core, 'setFailed').mockImplementation(() => {})
Expand Down Expand Up @@ -318,7 +319,7 @@ describe('label-sync job', () => {
const real = new Octokit({ auth: 'some-token', baseUrl: utils.api })
const refused = vi.fn().mockRejectedValue('offline')
const issues = Object.create(real.issues, {
createLabel: { value: (params: { name: string }) => (params.name === 'kind/bug' ? refused() : real.issues.createLabel(params)) },
createLabel: { value: (params: NonNullable<Parameters<typeof real.issues.createLabel>[0]>) => (params.name === 'kind/bug' ? refused() : real.issues.createLabel(params)) },
})
return Object.create(real, { issues: { value: issues } })
})
Expand Down
7 changes: 5 additions & 2 deletions __tests__/cronJobTest/lgtm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,10 @@ describe('cronLgtm', () => {
// Instead, we use it to gain the repo owner and url
const context = new utils.MockContext(pullReqOpenedEvent)

listPullReqs[0].labels[0].name = 'lgtm'
listPullReqs[0].labels.push({
// a copy: the shared fixture is what the default handlers above serve to every other test
const heldPrs = structuredClone(listPullReqs)
heldPrs[0].labels[0].name = 'lgtm'
heldPrs[0].labels.push({
id: 1,
node_id: '123',
url: 'https://api.github.com/repos/octocat/Hello-World/labels/hold',
Expand All @@ -189,6 +191,7 @@ describe('cronLgtm', () => {
color: 'f29513',
default: true,
})
routePulls(heldPrs)

const observeReq = new utils.ObserveRequest()
server.use(
Expand Down
2 changes: 2 additions & 0 deletions __tests__/issueCommentTest/assign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ afterAll(() => server.close())
describe('/assign', () => {
beforeEach(() => {
utils.setupActionsEnv('/assign')
// the tests below rewrite the shared fixture's comment; start each from the file's own body
issueCommentEventAssign.comment.body = '/assign'
})

it('handles self assigning with comment /assign', async () => {
Expand Down
5 changes: 4 additions & 1 deletion __tests__/issueCommentTest/handleIssueComment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ beforeAll(() => server.listen(utils.failOnUnhandledRequest))
// the dispatch tests below mock every command; the sweep that follows a command is stubbed the same way
let sweepStubs: ReturnType<typeof vi.spyOn>[]
beforeEach(() => {
// the dispatch tests rewrite the shared fixture's comment; start each from the file's own body
issueCommentEvent.comment.body = 'Meow meow!'
sweepStubs = [
vi.spyOn(requireMatchingLabel, 'checkRequiredLabels').mockResolvedValue(),
vi.spyOn(tide, 'tideOnComment').mockResolvedValue(),
Expand Down Expand Up @@ -405,7 +407,8 @@ it.each([
] as const)('listing only %s also enables %s', async (alias, base, mod, fn) => {
utils.setupActionsEnv(alias)

const spy = vi.spyOn(mod, fn).mockImplementation(() => Promise.resolve())
// the three modules export differently named handlers of one shape; the table pairs each with its own name
const spy = vi.spyOn(mod as Record<typeof fn, () => Promise<void>>, fn).mockImplementation(() => Promise.resolve())
const setFailed = vi.spyOn(core, 'setFailed').mockImplementation(() => {})

issueCommentEvent.comment.body = base
Expand Down
2 changes: 1 addition & 1 deletion __tests__/issueCommentTest/meowImageEdges.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function contextFor(body: string) {
// prefix, a url with only a username or only a password is refused, and the
// length bound is inclusive at 4096.
describe('/meow image validation edges', () => {
let createComment: ReturnType<typeof vi.spyOn<typeof comments, 'createComment'>>
let createComment: MockInstance<typeof comments.createComment>

beforeEach(() => {
utils.setupActionsEnv('/meow')
Expand Down
2 changes: 1 addition & 1 deletion __tests__/issueCommentTest/milestone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ describe('/milestone', () => {
})

it('still sends the clear when the issue has no milestone', async () => {
const payload = structuredClone(issueCommentEvent) as typeof issueCommentEvent & { issue: { milestone: unknown } }
const payload = structuredClone(issueCommentEvent) as Omit<typeof issueCommentEvent, 'issue'> & { issue: Omit<typeof issueCommentEvent.issue, 'milestone'> & { milestone: unknown } }
payload.comment.body = '/milestone clear'
payload.issue.milestone = null

Expand Down
2 changes: 2 additions & 0 deletions __tests__/issueCommentTest/unassign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ afterAll(() => server.close())
describe('/unassign', () => {
beforeEach(() => {
utils.setupActionsEnv('/unassign')
// the tests below rewrite the shared fixture's comment; start each from the file's own body
issueCommentEventUnassign.comment.body = '/unassign'
})

it('handles self unassignment with comment /unassign', async () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/missingNumberMessage.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Context } from '@actions/github/lib/context'
import type { Context } from '../src/utils/context'
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'

Expand Down
2 changes: 1 addition & 1 deletion __tests__/platform/portability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('a platform other than GitHub Actions', () => {
const actions = platform()

beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' })
server.listen(utils.failOnUnhandledRequest)
})
afterEach(() => {
server.resetHandlers()
Expand Down
2 changes: 1 addition & 1 deletion __tests__/platform/runState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function withState(): Platform {
}

describe('runState', () => {
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
beforeAll(() => server.listen(utils.failOnUnhandledRequest))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Expand Down
1 change: 1 addition & 0 deletions __tests__/plugins/approve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function pullOwners(ownersFiles: Record<string, string>, files: string[], author
return {
number: 1,
baseSha: 'basesha',
headSha: 'headsha',
author,
draft: false,
open: true,
Expand Down
1 change: 1 addition & 0 deletions __tests__/plugins/approveErrorPaths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function pullOwners(ownersFiles: Record<string, string>, files: string[], author
return {
number: 1,
baseSha: 'basesha',
headSha: 'headsha',
author,
draft: false,
open: true,
Expand Down
1 change: 1 addition & 0 deletions __tests__/plugins/approveGuards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function pullOwners(ownersFiles: Record<string, string>, files: string[], author
return {
number: 1,
baseSha: 'basesha',
headSha: 'headsha',
author,
draft: false,
open: true,
Expand Down
7 changes: 4 additions & 3 deletions __tests__/plugins/blunderbuss.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
import { handleIssueComment } from '../../src/issueComment/handleIssueComment'
import { autoCc, blunderbuss, blunderbussSettings, pickReviewers } from '../../src/plugins/blunderbuss'
import { handlePullReq, pullRequestHandlers } from '../../src/pullReq/handlePullReq'
import { mergeProwConfig } from '../../src/utils/config'
import issueCommentEvent from '../fixtures/issues/issueCommentEvent.json'
import labelFileContents from '../fixtures/labels/labelFileContentsResp.json'
import pullReqOpenedEvent from '../fixtures/pullReq/pullReqOpenedEvent.json'
Expand Down Expand Up @@ -117,7 +118,7 @@ describe('pickReviewers', () => {

describe('blunderbussSettings', () => {
it('applies the defaults', () => {
expect(blunderbussSettings({ labels: {}, require_matching_label: [], tide: {}, hold: {}, blunderbuss: {}, sources: [] })).toEqual({
expect(blunderbussSettings({ ...mergeProwConfig({}, {}), sources: [] })).toEqual({
request_count: 2,
max_request_count: undefined,
exclude_approvers: false,
Expand All @@ -128,11 +129,11 @@ describe('blunderbussSettings', () => {
})

it('reads delay as milliseconds', () => {
expect(blunderbussSettings({ labels: {}, require_matching_label: [], tide: {}, hold: {}, blunderbuss: { delay: '1h30m' }, sources: [] }).delay).toBe(90 * 60_000)
expect(blunderbussSettings({ ...mergeProwConfig({}, {}), blunderbuss: { delay: '1h30m' }, sources: [] }).delay).toBe(90 * 60_000)
})

it('lowercases ignore_authors', () => {
expect(blunderbussSettings({ labels: {}, require_matching_label: [], tide: {}, hold: {}, blunderbuss: { ignore_authors: ['Dependabot[bot]'] }, sources: [] }).ignore_authors).toEqual(['dependabot[bot]'])
expect(blunderbussSettings({ ...mergeProwConfig({}, {}), blunderbuss: { ignore_authors: ['Dependabot[bot]'] }, sources: [] }).ignore_authors).toEqual(['dependabot[bot]'])
})
})

Expand Down
4 changes: 2 additions & 2 deletions __tests__/plugins/lgtmBinding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ afterAll(() => server.close())
const repo = `${utils.api}/repos/Codertocat/Hello-World`
const headSha = pullReqOpenedEvent.pull_request.head.sha

function labeled(label: string, sender: Record<string, unknown> = { login: 'alice', type: 'User' }, action = 'labeled') {
function labeled(label: string, sender: { login: string, type: string } = { login: 'alice', type: 'User' }, action = 'labeled') {
return new utils.MockContext({ ...pullReqOpenedEvent, action, label: { name: label }, sender })
}

Expand Down Expand Up @@ -114,7 +114,7 @@ describe('lgtmOnPullRequest', () => {
})

it('throws when the payload has no head', async () => {
await expect(lgtmOnPullRequest(new utils.MockContext({ action: 'labeled', label: { name: 'lgtm' }, sender: { login: 'alice' } }))).rejects.toThrow('missing pull request head')
await expect(lgtmOnPullRequest(new utils.MockContext({ action: 'labeled', label: { name: 'lgtm' }, sender: { login: 'alice', type: 'User' } }))).rejects.toThrow('missing pull request head')
})
})

Expand Down
3 changes: 2 additions & 1 deletion __tests__/plugins/requireMatchingLabel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { handleIssues } from '../../src/issues/handleIssues'
import { platform, setPlatform } from '../../src/platform'
import { applicableRules, checkRequiredLabels, enforceRequiredLabels, evaluate, maxGracePeriodMs, parseDuration, requireMatchingLabel } from '../../src/plugins/requireMatchingLabel'
import { handlePullReq } from '../../src/pullReq/handlePullReq'
import { mergeProwConfig } from '../../src/utils/config'
import * as labeling from '../../src/utils/labeling'
import { newOctokit } from '../../src/utils/octokit'
import * as sleepModule from '../../src/utils/sleep'
Expand Down Expand Up @@ -108,7 +109,7 @@ function commentEvent(body: string, overrides: Record<string, unknown> = {}) {
}

function configWith(...rules: RequireMatchingLabel[]): ProwConfig {
return { labels: {}, require_matching_label: rules, tide: {}, hold: {}, blunderbuss: {}, sources: [] }
return { ...mergeProwConfig({}, {}), require_matching_label: rules, sources: [] }
}

const botComment = { id: 11, body: `Please add a kind label.\n\n${marker}`, user: { login: 'github-actions[bot]', type: 'Bot' } }
Expand Down
3 changes: 2 additions & 1 deletion __tests__/plugins/tide.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { MockInstance } from 'vitest'
import { Buffer } from 'node:buffer'
import * as core from '@actions/core'
import { http } from 'msw'
Expand Down Expand Up @@ -61,7 +62,7 @@ function observeMerge(status = 200, body: unknown = { merged: true }) {
return observe
}

let sleep: ReturnType<typeof vi.spyOn>
let sleep: MockInstance<typeof sleepModule.sleep>
let octokit: ReturnType<typeof newOctokit>
let context: utils.MockContext

Expand Down
3 changes: 2 additions & 1 deletion __tests__/plugins/tideGuards.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { MockInstance } from 'vitest'
import * as core from '@actions/core'
import { http } from 'msw'
import { setupServer } from 'msw/node'
Expand Down Expand Up @@ -81,7 +82,7 @@ function serveGraphql(queue: utils.MergeQueueFixture = {}) {
return calls
}

let sleep: ReturnType<typeof vi.spyOn>
let sleep: MockInstance<typeof sleepModule.sleep>
let octokit: ReturnType<typeof newOctokit>
let context: utils.MockContext

Expand Down
2 changes: 1 addition & 1 deletion __tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ vi.mock('@actions/core', { spy: true })
// through the same chai method.
chai.use((_chai, utils) => {
for (const method of ['throw', 'throws', 'Throw']) {
utils.overwriteMethod(_chai.Assertion.prototype, method, _super => function (this: object, ...args: unknown[]) {
utils.overwriteMethod(_chai.Assertion.prototype, method, (_super: (...args: unknown[]) => unknown) => function (this: object, ...args: unknown[]) {
const object: unknown = utils.flag(this, 'object')
const negate = Boolean(utils.flag(this, 'negate'))
const promise = utils.flag(this, 'promise')
Expand Down
25 changes: 23 additions & 2 deletions __tests__/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SetupServerApi } from 'msw/node'
import type { SetupServer } from 'msw/node'
import type { Context } from '../src/utils/context'
import { Buffer } from 'node:buffer'
import { http } from 'msw'

import { afterEach, beforeEach, expect } from 'vitest'
Expand Down Expand Up @@ -33,7 +34,7 @@ export const failOnUnhandledRequest = { onUnhandledRequest: 'error', onUnhandled
*
* @param server - the file's msw server
*/
export function failTestOnUnhandledRequests(server: SetupServerApi) {
export function failTestOnUnhandledRequests(server: SetupServer) {
const unhandled: string[] = []
let allowed = false
const record = ({ request }: { request: Request }) => {
Expand Down Expand Up @@ -104,6 +105,26 @@ export function noOrgOrRepoConfigExcept(...except: string[]) {
.map(source => http.get(contentsUrl(source), mockResponse(404, { message: 'Not Found' })))
}

/**
* configFoundAt serves an empty configuration file at the given probe and 404
* at every other one, so the tier that owns it ends with that single request.
*
* loadProwConfig probes the organization and repository tiers at once; when the
* tier a test is exercising rejects, the other tier's chain of probes keeps
* going after the test's await returns, and a probe started between tests is
* reported against whichever test runs next. Ending the other tier on its first
* probe leaves only a request that is already in flight when the rejection lands.
*
* @param source - `owner/repo:path` or a repo-relative path of the probe that exists
* @param others - further probes the caller serves itself
*/
export function configFoundAt(source: string, ...others: string[]) {
return [
http.get(contentsUrl(source), mockResponse(200, { type: 'file', encoding: 'base64', content: Buffer.from('{}\n').toString('base64') })),
...noOrgOrRepoConfigExcept(source, ...others),
]
}

/**
* repoHasLabels serves `GET /repos/Codertocat/Hello-World/labels` with the
* given labels on a single page (any `per_page`/`page` query is ignored), so
Expand Down
Loading
Loading