228 lines
6.7 KiB
TypeScript
228 lines
6.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { createApi } from '../../scripts/lib/github-api.mjs'
|
|
|
|
type FetchCall = {
|
|
url: string
|
|
method?: string
|
|
init?: { headers?: Record<string, string>; method?: string; body?: string }
|
|
body?: unknown
|
|
}
|
|
|
|
const ok = (body: unknown, status = 200) =>
|
|
new Response(typeof body === 'string' ? body : JSON.stringify(body), {
|
|
status,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
describe('github-api', () => {
|
|
it('builds the request with bearer token, JSON body and explicit User-Agent', async () => {
|
|
const calls: FetchCall[] = []
|
|
const api = createApi({
|
|
token: 'tok',
|
|
repository: 'fsolla/teqo',
|
|
fetchImpl: async (url, init) => {
|
|
calls.push({ url: String(url), init })
|
|
return ok({ number: 1, title: 't', body: '', state: 'open', node_id: 'PR_x' })
|
|
},
|
|
})
|
|
|
|
await api.getPullRequest(1)
|
|
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0].url).toBe('https://api.github.com/repos/fsolla/teqo/pulls/1')
|
|
expect(calls[0].init!.headers!.Authorization).toBe('Bearer tok')
|
|
expect(calls[0].init!.headers!['User-Agent']).toContain('teqo-agent-scripts')
|
|
})
|
|
|
|
it('fails closed when no token is available', async () => {
|
|
delete process.env.GITHUB_TOKEN
|
|
const api = createApi({ fetchImpl: async () => ok('{}') })
|
|
|
|
await expect(api.getPullRequest(1)).rejects.toThrow(/GITHUB_TOKEN/)
|
|
})
|
|
|
|
it('normalizes the pull request to the CLI contract (state/draft/nodeId/head/base)', async () => {
|
|
const api = createApi({
|
|
token: 'tok',
|
|
fetchImpl: async () =>
|
|
ok({
|
|
number: 7,
|
|
title: 't',
|
|
body: 'Closes #1',
|
|
state: 'closed',
|
|
merged_at: '2026-08-19T00:00:00Z',
|
|
draft: false,
|
|
mergeable: true,
|
|
node_id: 'PR_abc123',
|
|
head: { ref: 'OPS71-x', sha: 'deadbeef' },
|
|
base: { ref: 'main' },
|
|
}),
|
|
})
|
|
|
|
const pr = await api.getPullRequest(7)
|
|
|
|
expect(pr).toEqual({
|
|
number: 7,
|
|
title: 't',
|
|
body: 'Closes #1',
|
|
state: 'CLOSED',
|
|
merged: true,
|
|
draft: false,
|
|
mergeable: true,
|
|
nodeId: 'PR_abc123',
|
|
head: { ref: 'OPS71-x', sha: 'deadbeef' },
|
|
base: { ref: 'main' },
|
|
})
|
|
})
|
|
|
|
it('markPullRequestReady patches draft:false', async () => {
|
|
const calls: FetchCall[] = []
|
|
const api = createApi({
|
|
token: 'tok',
|
|
fetchImpl: async (url, init) => {
|
|
calls.push({ url: String(url), method: init?.method, body: init?.body })
|
|
return ok(null)
|
|
},
|
|
})
|
|
|
|
await api.markPullRequestReady(4)
|
|
|
|
expect(calls).toEqual([
|
|
{
|
|
url: 'https://api.github.com/repos/fsolla/teqo/pulls/4',
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ draft: false }),
|
|
},
|
|
])
|
|
})
|
|
|
|
it('createPullRequest posts a Ready PR (never draft) with the given body', async () => {
|
|
const calls: FetchCall[] = []
|
|
const api = createApi({
|
|
token: 'tok',
|
|
fetchImpl: async (url, init) => {
|
|
calls.push({ url: String(url), method: init?.method, body: init?.body })
|
|
return ok({ number: 9, title: 't', html_url: 'https://github.com/fsolla/teqo/pull/9' })
|
|
},
|
|
})
|
|
|
|
const created = await api.createPullRequest({
|
|
head: 'OPS71-x',
|
|
base: 'main',
|
|
title: 'OPS71 — x',
|
|
body: 'Closes #97',
|
|
})
|
|
|
|
expect(created).toEqual({
|
|
number: 9,
|
|
title: 't',
|
|
htmlUrl: 'https://github.com/fsolla/teqo/pull/9',
|
|
})
|
|
expect(calls[0].url).toBe('https://api.github.com/repos/fsolla/teqo/pulls')
|
|
expect(JSON.parse(String(calls[0].body))).toEqual({
|
|
head: 'OPS71-x',
|
|
base: 'main',
|
|
title: 'OPS71 — x',
|
|
body: 'Closes #97',
|
|
})
|
|
})
|
|
|
|
it('enableAutoMerge posts the GraphQL mutation with rebase', async () => {
|
|
const calls: FetchCall[] = []
|
|
const api = createApi({
|
|
token: 'tok',
|
|
fetchImpl: async (url, init) => {
|
|
calls.push({ url: String(url), body: init?.body })
|
|
return ok({ data: { enablePullRequestAutoMerge: { pullRequest: { number: 4 } } } })
|
|
},
|
|
})
|
|
|
|
await api.enableAutoMerge('PR_abc123')
|
|
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0].url).toBe('https://api.github.com/graphql')
|
|
const { query, variables } = JSON.parse(String(calls[0].body))
|
|
expect(query).toContain('enablePullRequestAutoMerge')
|
|
expect(query).toContain('mergeMethod')
|
|
expect(variables).toEqual({ id: 'PR_abc123', mergeMethod: 'REBASE' })
|
|
})
|
|
|
|
it('enableAutoMerge throws with the GraphQL error message', async () => {
|
|
const api = createApi({
|
|
token: 'tok',
|
|
fetchImpl: async () =>
|
|
ok({
|
|
data: null,
|
|
errors: [{ message: 'auto-merge is disabled on this repository' }],
|
|
}),
|
|
})
|
|
|
|
await expect(api.enableAutoMerge('PR_x')).rejects.toThrow(/auto-merge is disabled/)
|
|
})
|
|
|
|
it('getBranchProtection returns null on 404 and normalizes an existing rule', async () => {
|
|
let calls = 0
|
|
const api = createApi({
|
|
token: 'tok',
|
|
fetchImpl: async () => {
|
|
calls += 1
|
|
if (calls === 1) return ok('{"message":"Not Found"}', 404)
|
|
return ok({
|
|
url: 'https://api.github.com/repos/fsolla/teqo/branches/main/protection',
|
|
required_status_checks: {
|
|
strict: false,
|
|
checks: [{ context: 'CI (PR) / checks', app_id: null }],
|
|
contexts: ['CI (PR) / checks'],
|
|
},
|
|
enforce_admins: { enabled: true, url: '…' },
|
|
required_pull_request_reviews: null,
|
|
})
|
|
},
|
|
})
|
|
|
|
expect(await api.getBranchProtection('main')).toBeNull()
|
|
|
|
const rule = await api.getBranchProtection('main')
|
|
expect(rule).toEqual({
|
|
required_status_checks: {
|
|
strict: false,
|
|
contexts: ['CI (PR) / checks'],
|
|
},
|
|
enforce_admins: true,
|
|
required_pull_request_reviews: null,
|
|
})
|
|
})
|
|
|
|
it('updateBranchProtection PUTs the full desired rule', async () => {
|
|
const calls: FetchCall[] = []
|
|
const api = createApi({
|
|
token: 'tok',
|
|
fetchImpl: async (url, init) => {
|
|
calls.push({ url: String(url), method: init?.method })
|
|
return ok(null)
|
|
},
|
|
})
|
|
|
|
await api.updateBranchProtection({ enforce_admins: true }, 'main')
|
|
|
|
expect(calls[0].url).toBe('https://api.github.com/repos/fsolla/teqo/branches/main/protection')
|
|
expect(calls[0].method).toBe('PUT')
|
|
})
|
|
|
|
it('throws with the API error body on non-2xx', async () => {
|
|
let calls = 0
|
|
const api = createApi({
|
|
token: 'tok',
|
|
retries: 0,
|
|
fetchImpl: async () => {
|
|
calls += 1
|
|
return ok('{"message":"Bad credentials"}', 401)
|
|
},
|
|
})
|
|
|
|
await expect(api.getPullRequest(1)).rejects.toThrow(/401/)
|
|
await expect(api.getPullRequest(1)).rejects.toThrow(/Bad credentials/)
|
|
expect(calls).toBe(2)
|
|
})
|
|
})
|