Skip to content

Commit 1568d35

Browse files
committed
feat(cnb-delete-branch): 添加单元测试并配置 CI
1 parent 5f05a66 commit 1568d35

7 files changed

Lines changed: 791 additions & 21 deletions

File tree

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,19 @@ jobs:
3737
- run: pnpm run lint
3838

3939
- run: pnpm run typecheck
40+
41+
test:
42+
runs-on: ubuntu-latest
43+
steps:
44+
- uses: actions/checkout@v6
45+
46+
- uses: pnpm/action-setup@v6
47+
48+
- uses: actions/setup-node@v6
49+
with:
50+
node-version-file: .node-version
51+
cache: pnpm
52+
53+
- run: pnpm ci
54+
55+
- run: pnpm run test

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
"build": "pnpm -r run build",
88
"lint": "eslint",
99
"lint:fix": "eslint --fix",
10+
"test": "vitest run",
1011
"typecheck": "tsgo --noEmit"
1112
},
1213
"devDependencies": {
1314
"@antfu/eslint-config": "catalog:",
1415
"@typescript/native-preview": "catalog:",
15-
"eslint": "catalog:"
16+
"eslint": "catalog:",
17+
"vitest": "catalog:"
1618
}
1719
}

packages/cnb-delete-branch/dist/index.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { createRequire } from "node:module";
2+
import process$1 from "node:process";
3+
import { pathToFileURL } from "node:url";
24
import * as os$1 from "os";
35
import os, { EOL } from "os";
46
import * as fs from "fs";
@@ -16234,8 +16236,8 @@ async function main() {
1623416236
setFailed(error instanceof Error ? error.message : String(error));
1623516237
}
1623616238
}
16237-
main().catch((error) => {
16239+
if (process$1.argv[1] && import.meta.url === pathToFileURL(process$1.argv[1]).href) main().catch((error) => {
1623816240
setFailed(`cnb-delete-branch failed: ${error instanceof Error ? error.message : String(error)}`);
1623916241
});
1624016242
//#endregion
16241-
export {};
16243+
export { CNBRequestError, deleteBranch, encodePath, fetchCNB, getPullBranchName, getPullRepoPath, listPulls, main, patchPull };
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { PullRequest } from './types'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import {
4+
CNBRequestError,
5+
deleteBranch,
6+
encodePath,
7+
fetchCNB,
8+
getPullBranchName,
9+
getPullRepoPath,
10+
listPulls,
11+
patchPull,
12+
} from './index'
13+
14+
vi.mock('@actions/core', () => ({
15+
endGroup: vi.fn(),
16+
getInput: vi.fn(),
17+
info: vi.fn(),
18+
setFailed: vi.fn(),
19+
startGroup: vi.fn(),
20+
}))
21+
22+
function mockFetchOnce(status: number, body = '', statusText = ''): void {
23+
vi.mocked(fetch).mockResolvedValueOnce(new Response(body, { status, statusText }))
24+
}
25+
26+
function createPullRequest(number: string, ref = 'refs/heads/feature', repo = 'tdesign/test'): PullRequest {
27+
return {
28+
assignees: [],
29+
author: {} as PullRequest['author'],
30+
base: null,
31+
blocked_on: '',
32+
body: '',
33+
comment_count: 0,
34+
created_at: '',
35+
head: {
36+
ref,
37+
repo: {
38+
id: repo,
39+
name: repo.split('/').at(-1) || repo,
40+
path: repo,
41+
web_url: '',
42+
},
43+
sha: '',
44+
},
45+
is_wip: false,
46+
labels: [],
47+
last_acted_at: '',
48+
mergeable_state: '',
49+
merged_by: {} as PullRequest['merged_by'],
50+
number,
51+
repo: {} as PullRequest['repo'],
52+
review_count: 0,
53+
state: 'open',
54+
title: `PR ${number}`,
55+
updated_at: '',
56+
}
57+
}
58+
59+
describe('cnb-delete-branch', () => {
60+
beforeEach(() => {
61+
vi.stubGlobal('fetch', vi.fn())
62+
})
63+
64+
it('encodes repo paths but preserves path separators', () => {
65+
expect(encodePath('tdesign/workflows')).toBe('tdesign/workflows')
66+
expect(encodePath('tdesign/branch with space')).toBe('tdesign/branch%20with%20space')
67+
})
68+
69+
it('sends CNB JSON headers and parses JSON responses', async () => {
70+
mockFetchOnce(200, JSON.stringify({ ok: true }))
71+
72+
await expect(fetchCNB('token', '/tdesign/test/-/pulls')).resolves.toEqual({
73+
status: 200,
74+
data: { ok: true },
75+
})
76+
77+
expect(fetch).toHaveBeenCalledWith('https://api.cnb.cool/tdesign/test/-/pulls', {
78+
headers: {
79+
'Accept': 'application/json',
80+
'Authorization': 'Bearer token',
81+
'Content-Type': 'application/json',
82+
},
83+
})
84+
})
85+
86+
it('throws CNBRequestError for failed API responses', async () => {
87+
mockFetchOnce(403, JSON.stringify({ errmsg: 'forbidden' }), 'Forbidden')
88+
89+
await expect(fetchCNB('token', '/tdesign/test/-/pulls')).rejects.toMatchObject({
90+
message: '[CNB] GET /tdesign/test/-/pulls failed: forbidden',
91+
status: 403,
92+
})
93+
})
94+
95+
it('lists pull requests across pages', async () => {
96+
const firstPage = Array.from({ length: 100 }, (_, index) => createPullRequest(String(index + 1)))
97+
const secondPage = [createPullRequest('101')]
98+
mockFetchOnce(200, JSON.stringify(firstPage))
99+
mockFetchOnce(200, JSON.stringify(secondPage))
100+
101+
const pulls = await listPulls('token', 'tdesign/test', 'open')
102+
103+
expect(pulls).toHaveLength(101)
104+
expect(fetch).toHaveBeenNthCalledWith(
105+
1,
106+
'https://api.cnb.cool/tdesign/test/-/pulls?state=open&page=1&page_size=100',
107+
expect.any(Object),
108+
)
109+
expect(fetch).toHaveBeenNthCalledWith(
110+
2,
111+
'https://api.cnb.cool/tdesign/test/-/pulls?state=open&page=2&page_size=100',
112+
expect.any(Object),
113+
)
114+
})
115+
116+
it('patches pull request state through the CNB pull endpoint', async () => {
117+
mockFetchOnce(200, '{}')
118+
119+
await expect(patchPull('token', 'tdesign/test', '12', { state: 'closed' })).resolves.toBe(true)
120+
121+
expect(fetch).toHaveBeenCalledWith('https://api.cnb.cool/tdesign/test/-/pulls/12', expect.objectContaining({
122+
body: JSON.stringify({ state: 'closed' }),
123+
method: 'PATCH',
124+
}))
125+
})
126+
127+
it('treats missing branch as a successful no-op but rethrows other delete failures', async () => {
128+
mockFetchOnce(404, JSON.stringify({ errmsg: 'not found' }), 'Not Found')
129+
await expect(deleteBranch('token', 'tdesign/test', 'feature')).resolves.toBe(false)
130+
131+
mockFetchOnce(403, JSON.stringify({ errmsg: 'forbidden' }), 'Forbidden')
132+
await expect(deleteBranch('token', 'tdesign/test', 'feature')).rejects.toBeInstanceOf(CNBRequestError)
133+
})
134+
135+
it('reads pull branch and repo safely when head data is missing', () => {
136+
expect(getPullBranchName(createPullRequest('1', 'refs/heads/feature/test'))).toBe('feature/test')
137+
expect(getPullRepoPath(createPullRequest('1'))).toBe('tdesign/test')
138+
expect(getPullBranchName({ head: null } as PullRequest)).toBeUndefined()
139+
expect(getPullRepoPath({ head: null } as PullRequest)).toBeUndefined()
140+
})
141+
})

packages/cnb-delete-branch/index.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { PullRequest } from './types'
2+
import process from 'node:process'
3+
import { pathToFileURL } from 'node:url'
24
import * as core from '@actions/core'
35

46
const CNB_API_URL = 'https://api.cnb.cool'
@@ -9,7 +11,7 @@ interface CNBResponse<T> {
911
data?: T
1012
}
1113

12-
class CNBRequestError extends Error {
14+
export class CNBRequestError extends Error {
1315
constructor(message: string, public status: number) {
1416
super(message)
1517
this.name = 'CNBRequestError'
@@ -28,19 +30,19 @@ function parseJson<T>(text: string): T | undefined {
2830
}
2931
}
3032

31-
function encodePath(value: string): string {
33+
export function encodePath(value: string): string {
3234
return value.split('/').map(encodeURIComponent).join('/')
3335
}
3436

35-
function getPullRepoPath(pr: PullRequest): string | undefined {
37+
export function getPullRepoPath(pr: PullRequest): string | undefined {
3638
return pr.head?.repo?.path
3739
}
3840

39-
function getPullBranchName(pr: PullRequest): string | undefined {
41+
export function getPullBranchName(pr: PullRequest): string | undefined {
4042
return pr.head?.ref?.replace(/^refs\/heads\//, '')
4143
}
4244

43-
async function fetchCNB<T>(
45+
export async function fetchCNB<T>(
4446
token: string,
4547
path: string,
4648
options?: RequestInit,
@@ -74,7 +76,7 @@ async function fetchCNB<T>(
7476
}
7577
}
7678

77-
async function listPulls(token: string, repo: string, state: string): Promise<PullRequest[]> {
79+
export async function listPulls(token: string, repo: string, state: string): Promise<PullRequest[]> {
7880
const pulls: PullRequest[] = []
7981

8082
for (let page = 1; ; page += 1) {
@@ -94,7 +96,7 @@ async function listPulls(token: string, repo: string, state: string): Promise<Pu
9496
return pulls
9597
}
9698

97-
async function patchPull(
99+
export async function patchPull(
98100
token: string,
99101
repo: string,
100102
number: string,
@@ -107,7 +109,7 @@ async function patchPull(
107109
return Boolean(result)
108110
}
109111

110-
async function deleteBranch(token: string, repo: string, branch: string): Promise<boolean> {
112+
export async function deleteBranch(token: string, repo: string, branch: string): Promise<boolean> {
111113
try {
112114
await fetchCNB(token, `/${encodePath(repo)}/-/git/branches/${encodePath(branch)}`, {
113115
method: 'DELETE',
@@ -122,7 +124,7 @@ async function deleteBranch(token: string, repo: string, branch: string): Promis
122124
}
123125
}
124126

125-
async function main(): Promise<void> {
127+
export async function main(): Promise<void> {
126128
const repo = core.getInput('repo', { required: true })
127129
const branch = core.getInput('branch', { required: true })
128130
const token = core.getInput('token', { required: true })
@@ -175,6 +177,8 @@ async function main(): Promise<void> {
175177
}
176178
}
177179

178-
main().catch((error) => {
179-
core.setFailed(`cnb-delete-branch failed: ${error instanceof Error ? error.message : String(error)}`)
180-
})
180+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
181+
main().catch((error) => {
182+
core.setFailed(`cnb-delete-branch failed: ${error instanceof Error ? error.message : String(error)}`)
183+
})
184+
}

0 commit comments

Comments
 (0)