feat(contracts,api): page-mode pagination + synced profile on admin users list
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,7 @@ describe('账号查询 GET /admin/users (e2e)', () => {
|
||||
'hasMore',
|
||||
'items',
|
||||
'nextCursor',
|
||||
'total',
|
||||
]);
|
||||
// 管理列表包含前缀模式排除的非激活用户(本 spec 的排除组)。
|
||||
const adminAccounts = (
|
||||
@@ -285,4 +286,73 @@ describe('账号查询 GET /admin/users (e2e)', () => {
|
||||
expect(admin.status).toBe(200);
|
||||
expect(admin.body.items).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('page 模式:page/pageSize 偏移分页 + total;synced 档案快照合并;游标/前缀模式形状不受染', async () => {
|
||||
// 档案种子:albert 挂映射带 profile 快照(G4);alice 无映射。
|
||||
await prisma.external_user_mappings.create({
|
||||
data: {
|
||||
userId: albertId,
|
||||
sourceSystem: 'uniservice',
|
||||
externalUserId: 'ext-albert',
|
||||
sourceVersion: 1,
|
||||
profile: { sn: 'A001', email: 'albert@x.com', companyAbbr: '合塑' },
|
||||
},
|
||||
});
|
||||
// 'al' 前缀域恒 5 账号:alice/albert/aldis/alpend/allock(顺序按 createdAt
|
||||
// +ID 倒序,不依赖种子时序断言具体页位)。
|
||||
const page1 = await lookup(adminToken, { query: 'al', page: 1, pageSize: 2 });
|
||||
expect(page1.status).toBe(200);
|
||||
expect(page1.body.total).toBe(5);
|
||||
expect(page1.body.hasMore).toBe(false);
|
||||
expect(page1.body.nextCursor).toBeNull();
|
||||
expect(page1.body.items).toHaveLength(2);
|
||||
|
||||
const page2 = await lookup(adminToken, { query: 'al', page: 2, pageSize: 2 });
|
||||
const page3 = await lookup(adminToken, { query: 'al', page: 3, pageSize: 2 });
|
||||
expect(page2.body.items).toHaveLength(2);
|
||||
expect(page3.body.items).toHaveLength(1);
|
||||
|
||||
// 走全量:三页并集 = 5 账号、无重复。
|
||||
const all = [
|
||||
...(page1.body.items as { account: string }[]),
|
||||
...(page2.body.items as { account: string }[]),
|
||||
...(page3.body.items as { account: string }[]),
|
||||
];
|
||||
expect(new Set(all.map((item) => item.account)).size).toBe(5);
|
||||
|
||||
// synced 合并:albert(有映射)带快照且字段原样;其余行 synced=null。
|
||||
const albert = all
|
||||
.concat() as unknown as { account: string; synced: unknown }[];
|
||||
const albertRow = albert.find((item) => item.account === 'albert');
|
||||
expect(albertRow?.synced).toEqual({
|
||||
sn: 'A001',
|
||||
email: 'albert@x.com',
|
||||
companyAbbr: '合塑',
|
||||
});
|
||||
const aliceRow = albert.find((item) => item.account === 'alice');
|
||||
expect(aliceRow?.synced).toBeNull();
|
||||
|
||||
// 越界页:空 items、total 仍返回。
|
||||
const beyond = await lookup(adminToken, { query: 'al', page: 4, pageSize: 2 });
|
||||
expect(beyond.status).toBe(200);
|
||||
expect(beyond.body.items).toEqual([]);
|
||||
expect(beyond.body.total).toBe(5);
|
||||
|
||||
// 游标模式(不带 page):total=null(键存在但空——兼容旧客户端语义)。
|
||||
const cursorMode = await lookup(adminToken, { query: 'al', limit: 2 });
|
||||
expect(cursorMode.status).toBe(200);
|
||||
expect(cursorMode.body.total).toBeNull();
|
||||
|
||||
// 前缀模式条目干净:不合并 synced(邀请候选无需档案)。
|
||||
const prefix = await lookup(staffToken, { accountPrefix: 'al' });
|
||||
expect(prefix.status).toBe(200);
|
||||
for (const item of prefix.body.items as object[]) {
|
||||
expect('synced' in item).toBe(false);
|
||||
}
|
||||
|
||||
// 参数校验:page=0 / pageSize=101 / 非整数 → 400。
|
||||
expect((await lookup(adminToken, { page: 0 })).status).toBe(400);
|
||||
expect((await lookup(adminToken, { page: 1, pageSize: 101 })).status).toBe(400);
|
||||
expect((await lookup(adminToken, { page: 'x' })).status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user