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); +});