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
38 changes: 38 additions & 0 deletions __tests__/cronJobTest/labelSync.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Buffer } from 'node:buffer'
import * as core from '@actions/core'
import { Octokit } from '@octokit/rest'
import { http } from 'msw'
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'

import { handleCronJobs } from '../../src/cronJobs/handleCronJob'
import { labelSync } from '../../src/cronJobs/labelSync'
import * as octokitFactory from '../../src/utils/octokit'
import labelFileContents from '../fixtures/labels/labelFileContentsResp.json'
import listPullReqs from '../fixtures/pullReq/pullReqListPulls.json'
import * as utils from '../testUtils'

// the github client only ever rejects with an Error over HTTP, so the
// non-Error fallback of a refused write is only reachable through a stub
vi.mock('../../src/utils/octokit', { spy: true })

const server = setupServer()
beforeAll(() =>
server.listen({
Expand Down Expand Up @@ -270,6 +276,38 @@ describe('label-sync job', () => {
expect(setFailed).toHaveBeenCalledWith(expect.stringContaining('could not load prow config'))
})

it('logs that only the built-in defaults apply when no configuration is found', async () => {
server.use(...utils.noOrgOrRepoConfigExcept())
recordWrites([])
const debug = vi.spyOn(core, 'debug').mockImplementation(() => {})
vi.spyOn(core, 'info').mockImplementation(() => {})

await labelSync(dispatchContext())

expect(debug).toHaveBeenCalledWith(expect.stringContaining('labels from the built-in defaults only'))
})

it('records a write refused with a non-Error value by its string form', async () => {
serveConfig()
const writes = recordWrites([])
vi.mocked(octokitFactory.newOctokit).mockImplementationOnce((token) => {
const real = new Octokit({ auth: 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)) },
})
return Object.create(real, { issues: { value: issues } })
})
const logError = vi.spyOn(core, 'error').mockImplementation(() => {})
vi.spyOn(core, 'info').mockImplementation(() => {})

const run = labelSync(dispatchContext())

await expect(run).rejects.toThrow('1 label(s) could not be synced: kind/bug (offline)')
expect(logError).toHaveBeenCalledWith('label-sync: could not sync kind/bug: offline')
expect(writes.map(w => (w.body as { name: string }).name)).toEqual(complete.map(l => l.name).filter(n => n !== 'kind/bug'))
})

it('runs next to an unknown job, which still fails the run', async () => {
utils.setupJobsEnv('label-sync bogus')
serveConfig()
Expand Down
17 changes: 17 additions & 0 deletions __tests__/issueCommentTest/approve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,23 @@ reviewers:
expect(setFailed).toHaveBeenCalledWith(expect.stringContaining('you cannot approve your own PR'))
})

it('require_self_approval: a payload whose issue has no user is not treated as the author', async () => {
const writes = serve({
owners: { OWNERS: 'approvers:\n- alice\n' },
files: ['src/a.go'],
prowYaml: 'approve:\n require_self_approval: true\n',
comments: [{ body: '/approve', user: { login: 'alice' } }],
})
const event = prCommentEvent('/approve', 'alice')
delete (event.issue as { user?: unknown }).user

await handleIssueComment(new utils.MockContext(event))

await expect(writes.addLabels.called()).resolves.toBe('called')
expect(await writes.addLabels.body()).toEqual({ labels: ['approved'] })
expect(setFailed).not.toHaveBeenCalled()
})

it('fails when the repository lacks the approved label', async () => {
const writes = serve({ owners: { OWNERS: 'approvers:\n- alice\n' }, files: ['src/a.go'], comments: [{ body: '/approve', user: { login: 'alice' } }], repoLabels: ['lgtm'] })

Expand Down
25 changes: 25 additions & 0 deletions __tests__/label/lgtm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'

import { handleIssueComment } from '../../src/issueComment/handleIssueComment'
import * as lgtmBinding from '../../src/plugins/lgtmBinding'

import issuePayload from '../fixtures/issues/issue.json'

Expand All @@ -15,6 +16,10 @@ import labelFileContents from '../fixtures/labels/labelFileContentsResp.json'
import * as utils from '../testUtils'
import { prCommentEvent, prHandlers } from '../utils/ownersFixtures'

// bindLgtm wraps every HTTP failure in an Error; its non-Error fallback in
// /lgtm is only reachable when the binding itself rejects with a bare value
vi.mock('../../src/plugins/lgtmBinding', { spy: true })

const server = setupServer()
beforeAll(() =>
server.listen({
Expand Down Expand Up @@ -783,6 +788,26 @@ reviewers:
expect(setFailed).toHaveBeenCalledWith(expect.stringContaining('could not bind lgtm to headsha'))
})

it('a binding refused with a non-Error value fails the command with its string form and applies no label', async () => {
const label = new utils.ObserveRequest()
const reply = new utils.ObserveRequest()
server.use(
...prHandlers({}, ['src/file1.txt']),
http.post(`${repo}/issues/1/labels`, utils.mockResponse(200, [], label)),
http.post(`${repo}/issues/1/comments`, utils.mockResponse(201, {}, reply)),
)
vi.mocked(lgtmBinding.bindLgtm).mockRejectedValueOnce('status api offline')
const setFailed = vi.spyOn(core, 'setFailed').mockImplementation(() => {})
vi.spyOn(core, 'error').mockImplementation(() => {})

await handleIssueComment(new utils.MockContext(prCommentEvent('/lgtm')))

await expect(reply.called()).resolves.toBe('called')
expect(await reply.body().then(body => body.body)).toBe('status api offline')
await expect(label.notCalled()).resolves.toBe('not called')
expect(setFailed).toHaveBeenCalledWith(expect.stringContaining('status api offline'))
})

it('lgtm.bind_to_commit: false applies the label with no status call at all', async () => {
const file = structuredClone(labelFileContents)
file.content = Buffer.from('lgtm:\n bind_to_commit: false\n').toString('base64')
Expand Down
10 changes: 9 additions & 1 deletion __tests__/utils/mergeQueue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as core from '@actions/core'
import { Octokit } from '@octokit/rest'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'

import {
dequeue,
Expand Down Expand Up @@ -115,6 +115,14 @@ describe('queueState', () => {
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('could not read the merge queue state of pr #7; falling back to a direct merge: Field isMergeQueueEnabled doesn\'t exist on type PullRequest'))
})

it('reports a non-Error rejection of the query by its string form', async () => {
const offline = { graphql: vi.fn().mockRejectedValue('offline') } as unknown as Octokit

await expect(queueState(offline, context, 7)).resolves.toBeUndefined()

expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('falling back to a direct merge: offline'))
})

it('warns again after the warning state is reset', async () => {
graphql(null, 'boom')

Expand Down
Loading