test(api): user-sync e2e aligned to V1.1 initial credentials
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user