每支測試各自寫一次「啟動 stub、登記 close、組環境變數」已經重複三次,抽到 helpers 之後新增測試檔只要一行。各檔仍保留自己的預設路由,因為那是該檔的 情境設定,不該共用。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
232 lines
8.1 KiB
JavaScript
232 lines
8.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { runScript, pathWithOnly } from './helpers/run-script.js';
|
|
import { healthyRoutes, stubEnv as envFor, withStubGitea } from './helpers/stub-gitea.js';
|
|
|
|
const REPO = 'plugins/tea-sdlc';
|
|
|
|
async function withStub(t, overrides = {}) {
|
|
return withStubGitea(t, healthyRoutes(REPO, overrides));
|
|
}
|
|
|
|
|
|
// ── 第一層:執行環境 ────────────────────────────────────────────────
|
|
|
|
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 或標籤',
|
|
);
|
|
});
|