From 5327f5882e5c8d37f7c1afbfc11b84d83ba01276 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 06:06:07 +0000 Subject: [PATCH] =?UTF-8?q?test(project-add):=20=E8=A6=86=E8=93=8B?= =?UTF-8?q?=E5=8F=8D=E6=9F=A5=E3=80=81=E8=B2=BC=E7=B6=B2=E5=9D=80=E3=80=81?= =?UTF-8?q?=E6=97=A2=E6=9C=89=E7=9C=8B=E6=9D=BF=E4=B8=8D=E8=A2=AB=E8=A6=86?= =?UTF-8?q?=E8=93=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- test/project-add.test.js | 156 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 test/project-add.test.js diff --git a/test/project-add.test.js b/test/project-add.test.js new file mode 100644 index 0000000..ff3ec3c --- /dev/null +++ b/test/project-add.test.js @@ -0,0 +1,156 @@ +/** + * 把工作包放進 Gitea 專案看板。 + * + * Gitea 1.27 沒有「列出專案」的 endpoint,所以專案 id 只能反查:掃最近的議題, + * 從它們身上的 projects 欄位湊出 id→名稱對照。全空時就只能請使用者貼網址。 + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { runScript } from './helpers/run-script.js'; +import { healthyRoutes, stubEnv as envFor, withStubGitea } from './helpers/stub-gitea.js'; + +const REPO = 'plugins/tea-sdlc'; +const INDEX = 12; + +function routes(overrides = {}, { issueProjects = [], recent = null } = {}) { + const recentIssues = recent ?? [ + { number: 9, projects: [{ id: 7, title: '開發看板' }] }, + { number: 8, projects: [{ id: 8, title: '維運看板' }] }, + { number: 7, projects: [] }, + ]; + return healthyRoutes(REPO, { + [`GET /api/v1/repos/${REPO}/issues/${INDEX}`]: { + status: 200, + body: { number: INDEX, projects: issueProjects, html_url: 'https://example.com/12' }, + }, + [`GET /api/v1/repos/${REPO}/issues`]: { status: 200, body: recentIssues }, + [`PATCH /api/v1/repos/${REPO}/issues/${INDEX}`]: (req) => ({ + status: 200, + body: { number: INDEX, ...req.body }, + }), + ...overrides, + }); +} + +const withStub = (t, overrides = {}, options) => withStubGitea(t, routes(overrides, options)); + +const run = (args, stub) => + runScript('project-add.js', ['--repo', REPO, '--index', String(INDEX), ...args], { + env: envFor(stub), + }); + +const patchOf = (stub) => stub.requests.find((r) => r.method === 'PATCH' && !r.path.endsWith('/0')); + +// ── 以名稱指定 ───────────────────────────────────────────────────── + +test('以名稱指定看板,靠掃議題反查出 id', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run(['--project', '開發看板'], stub); + + assert.equal(code, 0, JSON.stringify(json)); + assert.deepEqual(patchOf(stub).body.projects, [7]); + assert.equal(json.data.project.id, 7); + assert.equal(json.data.project.title, '開發看板'); +}); + +test('反查只掃最近 50 筆議題', async (t) => { + const stub = await withStub(t); + + await run(['--project', '開發看板'], stub); + + const scan = stub.requests.find((r) => r.method === 'GET' && r.path.endsWith('/issues')); + assert.equal(scan.query.limit, '50'); + assert.equal(scan.query.state, 'all'); +}); + +test('反查不到名稱時,要求使用者貼專案網址', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run(['--project', '不存在的看板'], stub); + + assert.equal(code, 1); + assert.equal(json.error.code, 'PROJECT_NOT_FOUND'); + assert.match(json.error.message, /網址/); + assert.match(json.error.message, /開發看板/, '要列出反查到的看板'); +}); + +test('最近的議題全都沒有看板時,說清楚為什麼查不到', async (t) => { + const stub = await withStub(t, {}, { recent: [{ number: 9, projects: [] }] }); + + const { json } = await run(['--project', '開發看板'], stub); + + assert.equal(json.error.code, 'PROJECT_NOT_FOUND'); + assert.match(json.error.message, /沒有任何一顆掛在看板上/); + assert.match(json.error.message, /請直接貼專案網址/); +}); + +// ── 以網址指定 ───────────────────────────────────────────────────── + +test('直接貼專案網址時,取結尾的 id,不必反查', async (t) => { + const stub = await withStub(t); + + const { code } = await run(['--project', 'https://gitea.jsc.idv.tw/plugins/-/projects/7'], stub); + + assert.equal(code, 0); + assert.deepEqual(patchOf(stub).body.projects, [7]); + assert.equal( + stub.requests.some((r) => r.method === 'GET' && r.path.endsWith('/issues')), + false, + '網址已經帶了 id,不該再去掃議題', + ); +}); + +test('網址結尾不是數字時中止', async (t) => { + const stub = await withStub(t); + + const { json } = await run(['--project', 'https://gitea.jsc.idv.tw/plugins/-/projects/abc'], stub); + + assert.equal(json.error.code, 'PROJECT_NOT_FOUND'); +}); + +// ── 不覆蓋既有看板 ───────────────────────────────────────────────── + +test('議題原本就在別的看板上時,保留它並補上新的', async (t) => { + const stub = await withStub(t, {}, { issueProjects: [{ id: 8, title: '維運看板' }] }); + + await run(['--project', '開發看板'], stub); + + assert.deepEqual(patchOf(stub).body.projects.sort(), [7, 8], 'projects 是整份取代,漏了就等於把人踢出原看板'); +}); + +test('已經在目標看板上時不重發 PATCH', async (t) => { + const stub = await withStub(t, {}, { issueProjects: [{ id: 7, title: '開發看板' }] }); + + const { code, json } = await run(['--project', '開發看板'], stub); + + assert.equal(code, 0); + assert.equal(patchOf(stub), undefined); + assert.equal(json.data.alreadyThere, true); +}); + +// ── 不建立專案 ───────────────────────────────────────────────────── + +test('不建立專案:沒有任何非 GET 請求打到專案相關端點', async (t) => { + const stub = await withStub(t); + + await run(['--project', '開發看板'], stub); + + const creations = stub.requests.filter( + (r) => r.method !== 'GET' && /project/i.test(r.path), + ); + assert.deepEqual(creations, []); +}); + +// ── --dry-run ───────────────────────────────────────────────────── + +test('--dry-run 列出將送出的 PATCH,且不寫入', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run(['--project', '開發看板', '--dry-run'], stub); + + assert.equal(code, 0); + assert.equal(json.data.dryRun, true); + assert.deepEqual(json.data.requests[0].body.projects, [7], '預覽要看得出解析出來的 id'); + assert.equal(stub.requests.some((r) => r.method !== 'GET'), false); +});