From b7754d52d15bfb35e14e0f1149aea665c5aefe5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 22 Sep 2026 16:22:38 +0800 Subject: [PATCH] test(api): user-sync e2e aligned to V1.1 initial credentials Co-Authored-By: Claude Code --- services/api/test/user-sync.e2e-spec.ts | 50 +++++++++++++------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/services/api/test/user-sync.e2e-spec.ts b/services/api/test/user-sync.e2e-spec.ts index 0e1824a..4c348dc 100644 --- a/services/api/test/user-sync.e2e-spec.ts +++ b/services/api/test/user-sync.e2e-spec.ts @@ -3,8 +3,8 @@ // app.get(MockUserSource) 程序化注入(mock-user-source.ts 文件头约定)。 // 覆盖(T30 语义全量,红线逐条断言): // ① 首轮全量:3 用户 = 1 account 关联(本地直建账号建映射,不动其 -// 凭据/角色,会话保留)+ 2 新增待激活(credentialStatus=pending + -// 哨兵哈希 !:pending:);counts/水位/映射行; +// 凭据/角色,会话保留)+ 2 新增(V1.1:credentialStatus=active + +// 初始密码=账号后 6 位,可立即登录);counts/水位/映射行; // ② 改名幂等:版本更高 → displayName 更新;同版本重跑 → 全跳过(counts 0); // ③ 上游停用与恢复:disabled → users.sourceStatus=disabled + 撤全部会话 // (令牌 401 + 正确密码登录 401)且 **localLocked 不被写入**;恢复 @@ -22,8 +22,8 @@ // cron tick 跳过(无新批次);放行后完成; // ⑩ 增量 + 周期全量对账(RECONCILE_EVERY_N_RUNS=10):低版本行的新变更 // 对增量不可见 → 第 10 批 cursor=null 全量对账应用;缺席用户绝不停用; -// ⑪ §10 闭环(Task 6 联动):同步创建的 pending 用户登录 401(哨兵哈希 -// 不 500)→ activate → 仍 401(哈希不可猜)→ reset-password → 可登录; +// ⑪ V1.1 初始凭据闭环:同步即可用初始密码登录 → activate 幂等 → +// reset-password 轮换后初始密码失效(Task 6 端点联动); // ⑫ status/runs/errors API 形状 + runs 游标分页走全量 + 参数 400 + 非 // admin 403 / 匿名 401。 import { randomBytes } from 'node:crypto'; @@ -256,7 +256,7 @@ describe('用户定期同步 /admin/user-sync (e2e)', () => { ext('ext-2', 'sync-u2', displayName, version); const U3 = (version: number) => ext('ext-3', 'sync-u3', '赵三', version); - it('① 首轮全量:1 account 关联 + 2 新增待激活;counts/水位/映射/红线(不动凭据角色会话)', async () => { + it('① 首轮全量:1 account 关联 + 2 新增(V1.1 直接 active);counts/水位/映射/红线(不动凭据角色会话)', async () => { scenario([U1(1), U2(1), U3(1)]); // pageSize 2 → 两页(1,2 | 3),全 v1 const { run } = await triggerRun(); @@ -266,15 +266,18 @@ describe('用户定期同步 /admin/user-sync (e2e)', () => { expect(run.cursor).toBe('1'); // 水位 = 已提交最大版本 expect(run.finishedAt).not.toBeNull(); - // 新增:待激活 + 哨兵哈希 + staff 角色 + 源状态。 + // 新增(V1.1 初始凭据):直接 active + 初始密码=账号后 6 位(非哨兵哈希)。 for (const account of ['sync-u2', 'sync-u3']) { const user = await userByAccount(account); expect(user).not.toBeNull(); - expect(user?.credentialStatus).toBe('pending'); + expect(user?.credentialStatus).toBe('active'); expect(user?.role).toBe('staff'); expect(user?.sourceStatus).toBe('active'); - expect(user?.passwordHash.startsWith('!:pending:')).toBe(true); + expect(user?.passwordHash.startsWith('!:pending:')).toBe(false); } + // V1.1:同步完成即可用初始密码登录。 + const initialLogin = await login('sync-u2', 'sync-u2'.slice(-6)); + expect(initialLogin.status).toBe(200); // 关联:本地直建账号建映射 + 应用源 displayName/sourceStatus; // 凭据/角色/会话原样(红线 1:绝不写 password_hash/role)。 @@ -413,7 +416,7 @@ describe('用户定期同步 /admin/user-sync (e2e)', () => { const u4 = await userByAccount('sync-u4'); expect(u4).not.toBeNull(); - expect(u4?.credentialStatus).toBe('pending'); + expect(u4?.credentialStatus).toBe('active'); expect(u4?.displayName).toBe('钱四'); }); @@ -600,37 +603,36 @@ describe('用户定期同步 /admin/user-sync (e2e)', () => { ).toBe(0); }); - it('⑪ §10 闭环:pending 登录 401(哨兵哈希不 500)→ activate → 仍 401 → reset-password → 可登录', async () => { + it('⑪ V1.1 初始凭据闭环:同步即可登录 → activate 幂等 → reset-password 轮换初始密码失效', async () => { const u2 = await userByAccount('sync-u2'); - expect(u2?.credentialStatus).toBe('pending'); + expect(u2?.credentialStatus).toBe('active'); - // 待激活 + 哨兵哈希:任何口令 401(而非 argon2 解析 500),文案同密码错误。 - const guess = await login('sync-u2', 'Guessing-123'); - expect(guess.status).toBe(401); - expect(guess.body.code).toBe(ErrorCodes.AUTH_INVALID_CREDENTIALS); + // 初始密码 = 账号后 6 位:同步完成即可登录(V1.1 取代旧的哨兵待激活闭环)。 + const initial = await login('sync-u2', 'sync-u2'.slice(-6)); + expect(initial.status).toBe(200); + expect(typeof initial.body.sessionToken).toBe('string'); - // 激活(credentialStatus 维度):哈希仍为哨兵 → 依旧 401。 + // activate 对 active 用户幂等(200,不重复落审计——admin-users 语义)。 const activate = await request(app.getHttpServer()) .post(`/${API_PREFIX}/admin/users/${u2?.id}/activate`) .set('Authorization', `Bearer ${adminToken}`) .send({}); expect(activate.status).toBe(200); expect(activate.body.credentialStatus).toBe('active'); - const still = await login('sync-u2', 'Guessing-123'); - expect(still.status).toBe(401); - expect(still.body.code).toBe(ErrorCodes.AUTH_INVALID_CREDENTIALS); - // 初始凭据唯一出口 = 管理员 reset-password(生成)。 + // 管理员 reset-password 轮换:新凭据可登录,初始密码同步失效。 const reset = await request(app.getHttpServer()) .post(`/${API_PREFIX}/admin/users/${u2?.id}/reset-password`) .set('Authorization', `Bearer ${adminToken}`) .send({}); expect(reset.status).toBe(200); - const initial = reset.body.newPassword as string; - expect(initial.length).toBeGreaterThanOrEqual(12); - const ok = await login('sync-u2', initial); + const rotated = reset.body.newPassword as string; + expect(rotated.length).toBeGreaterThanOrEqual(12); + const ok = await login('sync-u2', rotated); expect(ok.status).toBe(200); - expect(typeof ok.body.sessionToken).toBe('string'); + const old = await login('sync-u2', 'sync-u2'.slice(-6)); + expect(old.status).toBe(401); + expect(old.body.code).toBe(ErrorCodes.AUTH_INVALID_CREDENTIALS); }); it('⑫ status/runs/errors API 形状 + runs 游标分页走全量 + 参数 400 + 权限矩阵', async () => {