test(腳本契約): 以子行程與 stub server 驗證契約與四層前置檢查
測試一律以子行程執行腳本、比對 stdout 的單行 JSON 與 exit code,因為那正是 七個平台共用的實際呼叫方式,不會因內部重構而破碎。 Gitea 以本機 stub server 替身並錄下每一筆請求,藉此斷言「腳本到底發了哪些 請求」——包含 --dry-run 不得發出任何請求、前一層檢查沒過就不再往下打、 寫入權探針不得挾帶任何要寫入的欄位。git 則在 .tmp/ 下的臨時 repo 跑真實 指令,比 mock 可信且成本低。 lib-exports.test.js 是唯一直接 import lib 的例外:冪等查重與 git 執行點在 CLI 邊界上還沒有消費者,檔頭已註明等 #4 與 #10 落地後即可縮小或移除。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 認證來源:預設沿用使用者既有的 `tea login`,不另外要求設定一組 token。
|
||||
* TEA_SDLC_API_BASE / TEA_SDLC_TOKEN 只是給測試與 CI 的覆寫出口。
|
||||
*/
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { runScript, tmpRoot } from './helpers/run-script.js';
|
||||
import { startStubGitea, healthyRoutes } from './helpers/stub-gitea.js';
|
||||
|
||||
const REPO = 'plugins/tea-sdlc';
|
||||
|
||||
/** 寫一份 tea 風格的 config.yml 到暫存目錄,回傳路徑 */
|
||||
function writeTeaConfig(logins) {
|
||||
mkdirSync(tmpRoot, { recursive: true });
|
||||
const dir = mkdtempSync(join(tmpRoot, 'tea-'));
|
||||
const path = join(dir, 'config.yml');
|
||||
const body = logins
|
||||
.map((l) =>
|
||||
[
|
||||
` - name: ${l.name}`,
|
||||
` url: ${l.url}`,
|
||||
` token: ${l.token}`,
|
||||
` default: ${l.default ?? false}`,
|
||||
` ssh_key: ""`,
|
||||
` user: ${l.user ?? 'tester'}`,
|
||||
].join('\n'),
|
||||
)
|
||||
.join('\n');
|
||||
writeFileSync(path, `logins:\n${body}\npreferences:\n editor: false\n`);
|
||||
return path;
|
||||
}
|
||||
|
||||
async function withStub(t) {
|
||||
const stub = await startStubGitea(healthyRoutes(REPO));
|
||||
t.after(() => stub.close());
|
||||
return stub;
|
||||
}
|
||||
|
||||
/** stub 的 base 是 .../api/v1,tea config 記的是站台根網址 */
|
||||
const siteUrl = (stub) => stub.base.replace(/\/api\/v1$/, '');
|
||||
|
||||
test('沒有環境變數覆寫時,採用 tea 設定中 default 為真的登入', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const config = writeTeaConfig([
|
||||
{ name: 'OTHER', url: 'https://gitea.example.com', token: 'wrong-token' },
|
||||
{ name: 'JSC', url: siteUrl(stub), token: 'config-token', default: true },
|
||||
]);
|
||||
|
||||
const { code, json } = await runScript('labels-list.js', ['--repo', REPO], {
|
||||
env: { TEA_SDLC_CONFIG: config },
|
||||
});
|
||||
|
||||
assert.equal(code, 0);
|
||||
assert.equal(json.ok, true);
|
||||
assert.equal(stub.requests[0].authorization, 'token config-token');
|
||||
});
|
||||
|
||||
test('--host 指定站台時,挑出對應的那一組登入', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const config = writeTeaConfig([
|
||||
{ name: 'JSC', url: siteUrl(stub), token: 'jsc-token' },
|
||||
{ name: 'HP', url: 'https://gitea.example.com', token: 'hp-token', default: true },
|
||||
]);
|
||||
|
||||
const { code, json } = await runScript(
|
||||
'labels-list.js',
|
||||
['--repo', REPO, '--host', siteUrl(stub)],
|
||||
{ env: { TEA_SDLC_CONFIG: config } },
|
||||
);
|
||||
|
||||
assert.equal(code, 0, JSON.stringify(json));
|
||||
assert.equal(stub.requests[0].authorization, 'token jsc-token');
|
||||
});
|
||||
|
||||
test('--host 指定的站台沒有對應登入時,明確說是哪個站台缺登入', async (t) => {
|
||||
const config = writeTeaConfig([{ name: 'JSC', url: 'https://gitea.example.com', token: 't', default: true }]);
|
||||
|
||||
const { code, json } = await runScript(
|
||||
'labels-list.js',
|
||||
['--repo', REPO, '--host', 'https://gitea.missing.tw'],
|
||||
{ env: { TEA_SDLC_CONFIG: config } },
|
||||
);
|
||||
|
||||
assert.equal(code, 1);
|
||||
assert.equal(json.error.code, 'LOGIN_INVALID');
|
||||
assert.match(json.error.message, /gitea\.missing\.tw/);
|
||||
});
|
||||
|
||||
test('tea 設定檔不存在時,訊息指向 tea login add', async (t) => {
|
||||
const { json } = await runScript('labels-list.js', ['--repo', REPO], {
|
||||
env: { TEA_SDLC_CONFIG: join(tmpRoot, 'definitely-not-here', 'config.yml') },
|
||||
});
|
||||
|
||||
assert.equal(json.error.code, 'LOGIN_INVALID');
|
||||
assert.match(json.error.message, /tea login add/);
|
||||
});
|
||||
|
||||
test('設定檔存在但沒有任何登入時,同樣指向 tea login add', async (t) => {
|
||||
mkdirSync(tmpRoot, { recursive: true });
|
||||
const dir = mkdtempSync(join(tmpRoot, 'tea-'));
|
||||
const path = join(dir, 'config.yml');
|
||||
writeFileSync(path, 'logins: []\npreferences:\n editor: false\n');
|
||||
|
||||
const { json } = await runScript('labels-list.js', ['--repo', REPO], {
|
||||
env: { TEA_SDLC_CONFIG: path },
|
||||
});
|
||||
|
||||
assert.equal(json.error.code, 'LOGIN_INVALID');
|
||||
assert.match(json.error.message, /tea login add/);
|
||||
});
|
||||
|
||||
test('只有一組登入且未標 default 時,就用那一組', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const config = writeTeaConfig([{ name: 'ONLY', url: siteUrl(stub), token: 'only-token' }]);
|
||||
|
||||
const { code } = await runScript('labels-list.js', ['--repo', REPO], {
|
||||
env: { TEA_SDLC_CONFIG: config },
|
||||
});
|
||||
|
||||
assert.equal(code, 0);
|
||||
assert.equal(stub.requests[0].authorization, 'token only-token');
|
||||
});
|
||||
Reference in New Issue
Block a user