test(api): user-sync e2e aligned to V1.1 initial credentials

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-22 16:22:38 +08:00
parent 42878f115b
commit b7754d52d1
+26 -24
View File
@@ -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 () => {