Skip to content
Merged
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
122 changes: 122 additions & 0 deletions __tests__/bundle/rootOwnersFallback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { FakeGithub } from './fakeGithub'
import { Buffer } from 'node:buffer'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'

import { start } from './fakeGithub'
import { comment, helpersFor, membershipReads, repo, token } from './helpers'
import { runBundle } from './runBundle'

vi.setConfig({ testTimeout: 30_000 })

// assertAuthorizedByOwnersOrMembership on an issue (not a pull request), driven through dist/index.js: when the
// root OWNERS read 404s the commenter falls back to org membership or collaborator status, and the read's other
// failure arms (a non-404 error, a body with no content) refuse the command by name. Also the 500 arms of the
// membership probes themselves: an org-membership read that fails unexpectedly warns and counts as "not a
// member", and an issue-comments read that fails unexpectedly warns and counts as "has not commented".
describe('dist/index.js OWNERS authorization on an issue without a root OWNERS file', () => {
const rootOwnersRead = `GET ${repo}/contents/OWNERS`
const orgRead = '/orgs/Codertocat/members/Codertocat'
const collaboratorRead = `${repo}/collaborators/Codertocat`
const commentPost = `POST ${repo}/issues/1/comments`
const labelPost = `POST ${repo}/issues/1/labels`
const labelsRead = `GET ${repo}/labels?per_page=100`
const notFound = { status: 404, body: { message: 'Not Found' } }
const serverError = { status: 500, body: { message: 'boom' } }
let gh: FakeGithub
const { calls, expectCommandThenConfig } = helpersFor(() => gh)

beforeAll(async () => {
gh = await start()
})
afterEach(() => gh.reset())
afterAll(() => gh.close())

function warnings(stdout: string) {
return stdout.split(/\r?\n/).filter(line => line.startsWith('::warning::')).map(line => line.slice('::warning::'.length))
}

function comments() {
return gh.requestsMatching('POST', /\/issues\/1\/comments$/).map(r => (r.body as { body: string }).body)
}

// Codertocat's `/lgtm` on issue #1, which some-author opened, so the commenter is not the author
async function lgtm() {
return runBundle({
eventName: 'issue_comment',
payload: comment('/lgtm', 'some-author'),
inputs: { ...token, 'prow-commands': '/lgtm' },
apiUrl: gh.url,
})
}

describe('/lgtm by a non-author', () => {
it('is authorized by collaborator status when the root OWNERS read 404s and the org-membership read fails unexpectedly', async () => {
gh.route('GET', rootOwnersRead.slice('GET '.length), notFound)
gh.route('GET', orgRead, serverError)
gh.route('GET', collaboratorRead, { status: 204 })
gh.route('GET', `${repo}/labels`, { status: 200, body: [{ name: 'lgtm' }] })
gh.route('POST', labelPost.slice('POST '.length), { status: 200, body: [] })

const result = await lgtm()

expect(result.status, result.stdout).toBe(0)
expect(result.errors).toEqual([])
expect(warnings(result.stdout).some(w => w.includes('status=500'))).toBe(true)
expect(gh.requestsMatching('POST', /\/issues\/1\/labels$/)[0].body).toEqual({ labels: ['lgtm'] })
expect(comments()).toEqual([])
// the root OWNERS read, the membership fallback, the label write, then the post-command sweep's config reads
expectCommandThenConfig([rootOwnersRead, ...membershipReads('Codertocat'), labelsRead, labelPost])
})

it('is refused without any membership read when the root OWNERS read fails with a non-404 error', async () => {
gh.route('GET', rootOwnersRead.slice('GET '.length), serverError)
gh.route('POST', commentPost.slice('POST '.length), { status: 201, body: {} })

const result = await lgtm()

expect(result.status, result.stdout).toBe(1)
expect(result.errors.some(e => e.includes('error checking for an OWNERS file at the root of the repository: HttpError: boom'))).toBe(true)
expect(comments()).toEqual(['Cannot apply the lgtm label because Error: error checking for an OWNERS file at the root of the repository: HttpError: boom'])
expect(gh.requestsMatching('GET', /\/orgs\/|\/collaborators\//)).toEqual([])
expectCommandThenConfig([rootOwnersRead, commentPost])
})

it('is refused without any membership read when the root OWNERS path is a directory, which the contents API lists without content', async () => {
gh.route('GET', rootOwnersRead.slice('GET '.length), {
status: 200,
body: [{ name: 'alice', path: 'OWNERS/alice', type: 'file', content: Buffer.from('x').toString('base64') }],
})
gh.route('POST', commentPost.slice('POST '.length), { status: 201, body: {} })

const result = await lgtm()

expect(result.status, result.stdout).toBe(1)
expect(result.errors.some(e => e.includes('invalid OWNERS file returned from GitHub API'))).toBe(true)
expect(comments()).toHaveLength(1)
expect(comments()[0]).toContain('Cannot apply the lgtm label because Error: invalid OWNERS file returned from GitHub API')
expect(gh.requestsMatching('GET', /\/orgs\/|\/collaborators\//)).toEqual([])
expectCommandThenConfig([rootOwnersRead, commentPost])
})
})

describe('/unassign by a non-author', () => {
it('warns and removes nothing when the commenter is not a member or collaborator and the issue-comments read fails unexpectedly', async () => {
gh.route('GET', orgRead, notFound)
gh.route('GET', collaboratorRead, notFound)
gh.route('GET', `${repo}/issues/1/comments`, serverError)

const result = await runBundle({
eventName: 'issue_comment',
payload: comment('/unassign @octocat', 'some-author'),
inputs: { ...token, 'prow-commands': '/unassign' },
apiUrl: gh.url,
})

expect(result.status, result.stdout).toBe(0)
expect(result.errors).toEqual([])
expect(warnings(result.stdout).some(w => w.includes('status=500'))).toBe(true)
expect(gh.requestsMatching('DELETE', /\/issues\/1\/assignees$/)).toEqual([])
expect(calls()).toEqual([...membershipReads('Codertocat'), `GET ${repo}/issues/1/comments`])
})
})
})
Loading