Archived
e2e:改用 Keycloak 服务账号 client_credentials 拿 token,4 分钟一换;断言进得了设计端与工作台
This commit is contained in:
+28
-12
@@ -25,7 +25,13 @@
|
||||
*/
|
||||
|
||||
const BASE = process.env.JCAI_E2E_BASE ?? 'http://localhost:8787';
|
||||
const PASSWORD = process.env.JCAI_SEED_PASSWORD ?? 'jcai-dev-2026';
|
||||
/** 身份归统一鉴权中心(ADR-012):e2e 用服务账号的 client_credentials 拿 token,不用人的账号 */
|
||||
const KC_URL = (process.env.KEYCLOAK_URL ?? '').replace(/\/+$/, '');
|
||||
const KC_REALM = process.env.KEYCLOAK_REALM ?? 'corp';
|
||||
const E2E_CLIENT_ID = process.env.KEYCLOAK_E2E_CLIENT_ID ?? '';
|
||||
const E2E_CLIENT_SECRET = process.env.KEYCLOAK_E2E_CLIENT_SECRET ?? '';
|
||||
/** access token 只活 5 分钟:超过 4 分钟就换一枚,免得长案例中途 401 */
|
||||
const TOKEN_REFRESH_MS = 4 * 60 * 1000;
|
||||
|
||||
/** 每套活动的案例:样例序号 + 人在件上要填的格子(按 pn 对行) */
|
||||
const SUITES = {
|
||||
@@ -503,12 +509,14 @@ const SUITES = {
|
||||
},
|
||||
};
|
||||
|
||||
let cookie = '';
|
||||
let bearer = '';
|
||||
let bearerAt = 0;
|
||||
|
||||
async function api(path, init = {}) {
|
||||
if (bearer !== '' && Date.now() - bearerAt > TOKEN_REFRESH_MS) await login();
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
...init,
|
||||
headers: { 'content-type': 'application/json', cookie, ...(init.headers ?? {}) },
|
||||
headers: { 'content-type': 'application/json', authorization: bearer, ...(init.headers ?? {}) },
|
||||
});
|
||||
const text = await res.text();
|
||||
let body;
|
||||
@@ -526,15 +534,23 @@ async function api(path, init = {}) {
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const res = await fetch(`${BASE}/api/login`, {
|
||||
if (KC_URL === '' || E2E_CLIENT_ID === '' || E2E_CLIENT_SECRET === '') {
|
||||
throw new Error('缺 KEYCLOAK_URL / KEYCLOAK_E2E_CLIENT_ID / KEYCLOAK_E2E_CLIENT_SECRET —— e2e 靠服务账号拿 token(ADR-012)');
|
||||
}
|
||||
const res = await fetch(`${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ account: 'AI', password: PASSWORD }),
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ grant_type: 'client_credentials', client_id: E2E_CLIENT_ID, client_secret: E2E_CLIENT_SECRET }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`登录失败:${res.status} ${await res.text()}`);
|
||||
const raw = res.headers.get('set-cookie') ?? '';
|
||||
cookie = raw.split(';')[0] ?? '';
|
||||
if (!cookie.includes('jcai_session')) throw new Error(`没拿到会话 cookie:${raw}`);
|
||||
if (!res.ok) throw new Error(`取 token 失败:${res.status} ${await res.text()}`);
|
||||
const { access_token: token } = await res.json();
|
||||
bearer = `Bearer ${token}`;
|
||||
bearerAt = Date.now();
|
||||
// 试跑要进设计端(测试台)、答卡要进工作台 —— 服务账号的角色不够,这里就先说清楚
|
||||
const me = await api('/api/me');
|
||||
for (const end of ['design', 'workbench']) {
|
||||
if (!me.body.ends.includes(end)) throw new Error(`e2e 服务账号进不了 ${end}:ends=${JSON.stringify(me.body.ends)} —— 给它 realm 角色 admin`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSample(activityId, index) {
|
||||
@@ -731,7 +747,7 @@ async function assertAssetFiles(taskId, spec) {
|
||||
if (empty) return { ok: false, note: `${key}(${format})是一份空壳 —— 渲得出形状,里头没内容` };
|
||||
|
||||
// 原字节也得取得到:下载到的是真 .docx / .xlsx
|
||||
const raw = await fetch(`${BASE}/api/desk/tasks/${taskId}/file?address=${encodeURIComponent(spec.address)}&key=${encodeURIComponent(key)}&raw=1`, { headers: { cookie } });
|
||||
const raw = await fetch(`${BASE}/api/desk/tasks/${taskId}/file?address=${encodeURIComponent(spec.address)}&key=${encodeURIComponent(key)}&raw=1`, { headers: { authorization: bearer } });
|
||||
const bytes = new Uint8Array(await raw.arrayBuffer());
|
||||
if (!raw.ok || bytes.length < 40) return { ok: false, note: `${key} 的原字节取不到(${raw.status})` };
|
||||
if ((format === 'docx' || format === 'xlsx') && !(bytes[0] === 0x50 && bytes[1] === 0x4b)) {
|
||||
@@ -772,7 +788,7 @@ async function assertArchived(taskId, expected) {
|
||||
return { ok: false, note: `${f.name} 存档里取不回来:${String(error.message).slice(0, 100)}` };
|
||||
}
|
||||
if (got.body.file?.kind === undefined) return { ok: false, note: `${f.name} 取回来是个空壳` };
|
||||
const raw = await fetch(`${BASE}${url}&raw=1`, { headers: { cookie } });
|
||||
const raw = await fetch(`${BASE}${url}&raw=1`, { headers: { authorization: bearer } });
|
||||
const bytes = new Uint8Array(await raw.arrayBuffer());
|
||||
if (!raw.ok || bytes.length < 40) return { ok: false, note: `${f.name} 的原件取不到(${raw.status} / ${bytes.length}B)` };
|
||||
if ((f.format === 'docx' || f.format === 'xlsx') && !(bytes[0] === 0x50 && bytes[1] === 0x4b)) {
|
||||
|
||||
Reference in New Issue
Block a user