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 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 对行) */
|
/** 每套活动的案例:样例序号 + 人在件上要填的格子(按 pn 对行) */
|
||||||
const SUITES = {
|
const SUITES = {
|
||||||
@@ -503,12 +509,14 @@ const SUITES = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let cookie = '';
|
let bearer = '';
|
||||||
|
let bearerAt = 0;
|
||||||
|
|
||||||
async function api(path, init = {}) {
|
async function api(path, init = {}) {
|
||||||
|
if (bearer !== '' && Date.now() - bearerAt > TOKEN_REFRESH_MS) await login();
|
||||||
const res = await fetch(`${BASE}${path}`, {
|
const res = await fetch(`${BASE}${path}`, {
|
||||||
...init,
|
...init,
|
||||||
headers: { 'content-type': 'application/json', cookie, ...(init.headers ?? {}) },
|
headers: { 'content-type': 'application/json', authorization: bearer, ...(init.headers ?? {}) },
|
||||||
});
|
});
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let body;
|
let body;
|
||||||
@@ -526,15 +534,23 @@ async function api(path, init = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login() {
|
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',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||||
body: JSON.stringify({ account: 'AI', password: PASSWORD }),
|
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()}`);
|
if (!res.ok) throw new Error(`取 token 失败:${res.status} ${await res.text()}`);
|
||||||
const raw = res.headers.get('set-cookie') ?? '';
|
const { access_token: token } = await res.json();
|
||||||
cookie = raw.split(';')[0] ?? '';
|
bearer = `Bearer ${token}`;
|
||||||
if (!cookie.includes('jcai_session')) throw new Error(`没拿到会话 cookie:${raw}`);
|
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) {
|
async function loadSample(activityId, index) {
|
||||||
@@ -731,7 +747,7 @@ async function assertAssetFiles(taskId, spec) {
|
|||||||
if (empty) return { ok: false, note: `${key}(${format})是一份空壳 —— 渲得出形状,里头没内容` };
|
if (empty) return { ok: false, note: `${key}(${format})是一份空壳 —— 渲得出形状,里头没内容` };
|
||||||
|
|
||||||
// 原字节也得取得到:下载到的是真 .docx / .xlsx
|
// 原字节也得取得到:下载到的是真 .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());
|
const bytes = new Uint8Array(await raw.arrayBuffer());
|
||||||
if (!raw.ok || bytes.length < 40) return { ok: false, note: `${key} 的原字节取不到(${raw.status})` };
|
if (!raw.ok || bytes.length < 40) return { ok: false, note: `${key} 的原字节取不到(${raw.status})` };
|
||||||
if ((format === 'docx' || format === 'xlsx') && !(bytes[0] === 0x50 && bytes[1] === 0x4b)) {
|
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)}` };
|
return { ok: false, note: `${f.name} 存档里取不回来:${String(error.message).slice(0, 100)}` };
|
||||||
}
|
}
|
||||||
if (got.body.file?.kind === undefined) return { ok: false, note: `${f.name} 取回来是个空壳` };
|
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());
|
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 (!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)) {
|
if ((f.format === 'docx' || f.format === 'xlsx') && !(bytes[0] === 0x50 && bytes[1] === 0x4b)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user