import test from 'node:test'; import assert from 'node:assert/strict'; import { runScript, pathWithOnly } from './helpers/run-script.js'; import { startStubGitea, healthyRoutes } from './helpers/stub-gitea.js'; const REPO = 'plugins/tea-sdlc'; async function withStub(t, overrides = {}) { const stub = await startStubGitea(healthyRoutes(REPO, overrides)); t.after(() => stub.close()); return stub; } const envFor = (stub) => ({ TEA_SDLC_API_BASE: stub.base, TEA_SDLC_TOKEN: 'stub-token' }); // ── 第一層:執行環境 ──────────────────────────────────────────────── test('第一層:PATH 上沒有 git 時中止,錯誤訊息指名 git', async (t) => { const stub = await withStub(t); const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub), path: pathWithOnly(['tea']), }); assert.equal(code, 1); assert.equal(json.error.code, 'ENV_MISSING'); assert.match(json.error.message, /git/); assert.equal(stub.requests.length, 0, '環境不通過就不該發請求'); }); test('第一層:PATH 上沒有 tea 時中止,錯誤訊息指名 tea 並給出安裝指引', async (t) => { const stub = await withStub(t); const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub), path: pathWithOnly(['git']), }); assert.equal(code, 1); assert.equal(json.error.code, 'ENV_MISSING'); assert.match(json.error.message, /tea/); }); // ── 第二層:Gitea 登入 ───────────────────────────────────────────── test('第二層:token 無效時中止,訊息指向 tea login', async (t) => { const stub = await withStub(t, { 'GET /api/v1/user': { status: 401, body: { message: 'unauthorized' } }, }); const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.equal(code, 1); assert.equal(json.error.code, 'LOGIN_INVALID'); assert.match(json.error.message, /tea login/); }); test('第二層:找不到任何登入資訊時中止', async (t) => { const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: { TEA_SDLC_API_BASE: '', TEA_SDLC_TOKEN: '', TEA_SDLC_CONFIG: '/nonexistent/tea.yml' }, }); assert.equal(code, 1); assert.equal(json.error.code, 'LOGIN_INVALID'); }); // ── 第三層:issues unit 寫入權 ───────────────────────────────────── test('第三層:repo 不存在或讀不到時,錯誤碼與權限問題分得開', async (t) => { const stub = await withStub(t, { [`GET /api/v1/repos/${REPO}`]: { status: 404, body: { message: 'not found' } }, }); const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.equal(code, 1); assert.equal(json.error.code, 'REPO_NOT_FOUND'); }); test('第三層:repo 關閉議題功能時中止,訊息指出開啟位置', async (t) => { const stub = await withStub(t, { [`GET /api/v1/repos/${REPO}`]: { status: 200, body: { has_issues: false, permissions: { admin: false, push: true, pull: true }, internal_tracker: { enable_time_tracker: true }, }, }, }); const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.equal(code, 1); assert.equal(json.error.code, 'ISSUES_UNIT_OFF'); assert.match(json.error.message, /Settings/); }); test('第三層:對 issues unit 實測寫入權,被擋下時中止', async (t) => { const stub = await withStub(t, { [`PATCH /api/v1/repos/${REPO}/issues/0`]: { status: 403, body: { message: 'forbidden' } }, }); const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.equal(code, 1); assert.equal(json.error.code, 'NO_ISSUE_WRITE'); assert.match(json.error.message, /issues/i); }); test('第三層:不能只看 permissions.push——push 為真但 issues unit 被擋,仍須失敗', async (t) => { const stub = await withStub(t, { [`GET /api/v1/repos/${REPO}`]: { status: 200, body: { has_issues: true, // team 的 unit 權限可獨立於 repo 的 push 權限,所以這裡是真的也不算數 permissions: { admin: false, push: true, pull: true }, internal_tracker: { enable_time_tracker: true }, }, }, [`PATCH /api/v1/repos/${REPO}/issues/0`]: { status: 403, body: { message: 'forbidden' } }, }); const { json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.equal(json.error.code, 'NO_ISSUE_WRITE'); }); test('第三層:探針打在不存在的議題 index 0,確保無副作用', async (t) => { const stub = await withStub(t); await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); const probes = stub.requests.filter((r) => r.method === 'PATCH'); assert.equal(probes.length, 1); assert.equal(probes[0].path, `/api/v1/repos/${REPO}/issues/0`); assert.deepEqual(probes[0].body, {}, '探針不得挾帶任何要寫入的欄位'); }); // ── 第四層:時間追蹤 ─────────────────────────────────────────────── test('第四層:時間追蹤未開啟時中止,訊息指出 Enable Time Tracker 的位置', async (t) => { const stub = await withStub(t, { [`GET /api/v1/repos/${REPO}`]: { status: 200, body: { has_issues: true, permissions: { admin: false, push: true, pull: true }, internal_tracker: { enable_time_tracker: false }, }, }, }); const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.equal(code, 1); assert.equal(json.error.code, 'TIME_TRACKER_OFF'); assert.match(json.error.message, /Settings → Advanced Settings → Enable Time Tracker/); }); // ── 四層之間 ─────────────────────────────────────────────────────── test('四層的錯誤碼兩兩相異,呼叫端分得出是哪一層壞了', async (t) => { const cases = [ { code: 'ENV_MISSING', run: async () => { const stub = await withStub(t); return runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub), path: pathWithOnly(['git']), }); }, }, { code: 'LOGIN_INVALID', run: async () => { const stub = await withStub(t, { 'GET /api/v1/user': { status: 401, body: {} }, }); return runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); }, }, { code: 'NO_ISSUE_WRITE', run: async () => { const stub = await withStub(t, { [`PATCH /api/v1/repos/${REPO}/issues/0`]: { status: 403, body: {} }, }); return runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); }, }, { code: 'TIME_TRACKER_OFF', run: async () => { const stub = await withStub(t, { [`GET /api/v1/repos/${REPO}`]: { status: 200, body: { has_issues: true, permissions: { admin: true, push: true, pull: true }, internal_tracker: { enable_time_tracker: false }, }, }, }); return runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); }, }, ]; const seen = []; for (const { code, run } of cases) { const { json } = await run(); assert.equal(json.error.code, code); seen.push(json.error.code); } assert.equal(new Set(seen).size, seen.length); }); test('前一層失敗就停手,不會再往下打', async (t) => { const stub = await withStub(t, { 'GET /api/v1/user': { status: 401, body: {} }, }); await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.deepEqual( stub.requests.map((r) => r.path), ['/api/v1/user'], '登入檢查沒過就不該再查 repo 或標籤', ); });