2026-09-18 08:54:44 +08:00
|
|
|
|
// services/api/test/users-lookup.e2e-spec.ts —— P1b Task 4 账号前缀查询 e2e
|
2026-09-20 15:56:38 +08:00
|
|
|
|
// (先写后实现);P4a Task 1 契约统一后同步修订。真实 app + MySQL。覆盖:
|
|
|
|
|
|
// ① 前缀命中:prefix 'al' → albert+alice(account 升序);条目为统一
|
|
|
|
|
|
// AdminUser 字段集(id/account/displayName + 治理字段 role/sourceStatus/
|
|
|
|
|
|
// localLocked/credentialStatus/locale/createdAt;无邮箱/手机/凭据字段);
|
|
|
|
|
|
// 响应为 AdminUserList(items + nextCursor/hasMore 游标元信息);
|
2026-09-18 08:54:44 +08:00
|
|
|
|
// ② 账号资格过滤:credentialStatus=pending / sourceStatus=disabled /
|
|
|
|
|
|
// localLocked=true 均排除(只留「激活」用户);
|
|
|
|
|
|
// ③ 大小写敏感(库级 utf8mb4_0900_bin 二进制校对,契约已注明):'AL' → 空、
|
|
|
|
|
|
// 'carol' → 空、'Carol' → 命中;
|
|
|
|
|
|
// ④ LIKE 通配符转义(ESCAPE 子句):前缀中的 _ / % 按字面匹配,不作单字符/
|
|
|
|
|
|
// 任意串通配(esc_ 仅命中字面下划线账号;al% 空);
|
2026-09-20 19:31:00 +08:00
|
|
|
|
// ⑤ 参数边界:accountPrefix 缺失 → 管理列表模式分流(staff 403 / admin
|
|
|
|
|
|
// 200,Task 6 双模式收口后语义)/1 字符/65 字符 → 400;2 与 64 字符合法;limit 缺省 20(P1b 时
|
2026-09-20 15:56:38 +08:00
|
|
|
|
// 为 10,P4a 统一分页 bounds)、1..100 界内、0/101/非整数 → 400;
|
2026-09-18 08:54:44 +08:00
|
|
|
|
// ⑥ 访问策略(P1b 决策,契约描述):未认证 401;任何已认证工作人员(含非
|
|
|
|
|
|
// admin 的 staff)200;admin 200。
|
|
|
|
|
|
import { randomBytes } from 'node:crypto';
|
|
|
|
|
|
import { type INestApplication } from '@nestjs/common';
|
|
|
|
|
|
import { Test } from '@nestjs/testing';
|
|
|
|
|
|
import request from 'supertest';
|
|
|
|
|
|
import { PrismaClient } from '@photo/db-cn';
|
|
|
|
|
|
import { ErrorCodes } from '@photo/contracts';
|
|
|
|
|
|
import { AppCnModule } from '../src/app-cn.module';
|
|
|
|
|
|
import { hashSessionToken } from '../src/auth/session.guard';
|
|
|
|
|
|
import { API_PREFIX } from '../src/shared/constants';
|
|
|
|
|
|
import { resetDb } from './reset';
|
|
|
|
|
|
|
|
|
|
|
|
jest.setTimeout(120_000);
|
|
|
|
|
|
|
|
|
|
|
|
describe('账号查询 GET /admin/users (e2e)', () => {
|
|
|
|
|
|
let app: INestApplication;
|
|
|
|
|
|
let prisma: PrismaClient;
|
|
|
|
|
|
|
|
|
|
|
|
let staffToken: string; // 非 admin 工作人员(访问策略断言用)
|
|
|
|
|
|
let adminToken: string;
|
|
|
|
|
|
|
|
|
|
|
|
let aliceId: string;
|
|
|
|
|
|
let albertId: string;
|
|
|
|
|
|
let carolId: string;
|
|
|
|
|
|
let escUnderscoreId: string;
|
|
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
|
await resetDb();
|
|
|
|
|
|
const moduleRef = await Test.createTestingModule({
|
|
|
|
|
|
imports: [AppCnModule],
|
|
|
|
|
|
}).compile();
|
|
|
|
|
|
app = moduleRef.createNestApplication();
|
|
|
|
|
|
app.setGlobalPrefix(API_PREFIX);
|
|
|
|
|
|
await app.init();
|
|
|
|
|
|
|
|
|
|
|
|
prisma = new PrismaClient();
|
|
|
|
|
|
|
|
|
|
|
|
const mkUser = (
|
|
|
|
|
|
account: string,
|
|
|
|
|
|
displayName: string,
|
|
|
|
|
|
extra: {
|
|
|
|
|
|
role?: string;
|
|
|
|
|
|
sourceStatus?: string;
|
|
|
|
|
|
credentialStatus?: string;
|
|
|
|
|
|
localLocked?: boolean;
|
|
|
|
|
|
} = {},
|
|
|
|
|
|
) =>
|
|
|
|
|
|
prisma.users.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
account,
|
|
|
|
|
|
passwordHash: 'x',
|
|
|
|
|
|
displayName,
|
|
|
|
|
|
...extra,
|
|
|
|
|
|
},
|
|
|
|
|
|
select: { id: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
aliceId = (await mkUser('alice', '爱丽')).id;
|
|
|
|
|
|
albertId = (await mkUser('albert', '艾伯')).id;
|
|
|
|
|
|
await mkUser('bob', '博伟');
|
|
|
|
|
|
carolId = (await mkUser('Carol', '卡萝')).id; // 大写 C——大小写敏感断言
|
|
|
|
|
|
// 资格排除组(前缀均 'al' 命中域,但不应出现在结果里)。
|
|
|
|
|
|
await mkUser('aldis', '停用者', { sourceStatus: 'disabled' });
|
|
|
|
|
|
await mkUser('alpend', '待激活', { credentialStatus: 'pending' });
|
|
|
|
|
|
await mkUser('allock', '锁定者', { localLocked: true });
|
|
|
|
|
|
// LIKE 通配符转义断言组:esc_ 前缀若未转义 _,会同时命中 escshape。
|
|
|
|
|
|
escUnderscoreId = (await mkUser('esc_ape', '下划线')).id;
|
|
|
|
|
|
await mkUser('escshape', '形状');
|
|
|
|
|
|
// 默认 limit=10 截断断言组:12 个 zz-look-* 账号(升序 01..12)。
|
|
|
|
|
|
for (let i = 1; i <= 12; i++) {
|
|
|
|
|
|
await mkUser(`zz-look-${String(i).padStart(2, '0')}`, `批量${i}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 请求方:staff(非 admin)与 admin。
|
|
|
|
|
|
const staffId = (await mkUser('ul-staff', '陆石')).id;
|
|
|
|
|
|
const adminId = (await mkUser('ul-admin', '路安', { role: 'admin' })).id;
|
|
|
|
|
|
|
|
|
|
|
|
const mkToken = async (userId: string): Promise<string> => {
|
|
|
|
|
|
const token = randomBytes(32).toString('hex');
|
|
|
|
|
|
await prisma.sessions.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
userId,
|
|
|
|
|
|
tokenHash: hashSessionToken(token),
|
|
|
|
|
|
expiresAt: new Date(Date.now() + 3_600_000),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
return token;
|
|
|
|
|
|
};
|
|
|
|
|
|
staffToken = await mkToken(staffId);
|
|
|
|
|
|
adminToken = await mkToken(adminId);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
|
await app.close();
|
|
|
|
|
|
await prisma.$disconnect();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function lookup(
|
|
|
|
|
|
token: string | null,
|
|
|
|
|
|
query?: Record<string, string | number>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
const req = request(app
|
|
|
|
|
|
.getHttpServer())
|
|
|
|
|
|
.get(`/${API_PREFIX}/admin/users`);
|
|
|
|
|
|
if (token !== null) {
|
|
|
|
|
|
req.set('Authorization', `Bearer ${token}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return req.query(query ?? {});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-20 15:56:38 +08:00
|
|
|
|
it("前缀 'al' → albert+alice 升序;禁用/待激活/锁定排除;条目与响应形状(AdminUserList)", async () => {
|
2026-09-18 08:54:44 +08:00
|
|
|
|
const res = await lookup(staffToken, { accountPrefix: 'al' });
|
|
|
|
|
|
expect(res.status).toBe(200);
|
2026-09-20 15:56:38 +08:00
|
|
|
|
// 响应为统一 AdminUserList(P4a):items + 游标元信息(前缀模式即取即用
|
|
|
|
|
|
// → nextCursor=null / hasMore=false)。
|
|
|
|
|
|
expect(Object.keys(res.body).sort()).toEqual(['hasMore', 'items', 'nextCursor']);
|
|
|
|
|
|
expect(res.body.nextCursor).toBeNull();
|
|
|
|
|
|
expect(res.body.hasMore).toBe(false);
|
|
|
|
|
|
const items = res.body.items as Record<string, unknown>[];
|
2026-09-18 08:54:44 +08:00
|
|
|
|
// account 升序:albert < alice;恰好两条(排除组全不出现在结果)。
|
|
|
|
|
|
expect(items.map((item) => item.account)).toEqual(['albert', 'alice']);
|
|
|
|
|
|
expect(items.map((item) => item.id)).toEqual([albertId, aliceId]);
|
|
|
|
|
|
expect(items.map((item) => item.displayName)).toEqual(['艾伯', '爱丽']);
|
2026-09-20 15:56:38 +08:00
|
|
|
|
// 条目形状:AdminUser 字段集(治理字段随 P4a 统一暴露;仍无邮箱/手机/
|
2026-09-20 16:36:19 +08:00
|
|
|
|
// 密码哈希等凭据字段)。注意数组序 = Array#sort 的码点序(大小写敏感):
|
|
|
|
|
|
// 'localLocked'(大写 L,0x4C)排在 'locale'(e,0x65)之前。
|
2026-09-18 08:54:44 +08:00
|
|
|
|
for (const item of items) {
|
|
|
|
|
|
expect(Object.keys(item).sort()).toEqual([
|
|
|
|
|
|
'account',
|
2026-09-20 15:56:38 +08:00
|
|
|
|
'createdAt',
|
|
|
|
|
|
'credentialStatus',
|
2026-09-18 08:54:44 +08:00
|
|
|
|
'displayName',
|
|
|
|
|
|
'id',
|
2026-09-20 15:56:38 +08:00
|
|
|
|
'localLocked',
|
2026-09-20 16:36:19 +08:00
|
|
|
|
'locale',
|
2026-09-20 15:56:38 +08:00
|
|
|
|
'role',
|
|
|
|
|
|
'sourceStatus',
|
2026-09-18 08:54:44 +08:00
|
|
|
|
]);
|
2026-09-20 15:56:38 +08:00
|
|
|
|
expect(item.role).toBe('staff');
|
|
|
|
|
|
expect(item.sourceStatus).toBe('local');
|
|
|
|
|
|
expect(item.localLocked).toBe(false);
|
|
|
|
|
|
expect(item.credentialStatus).toBe('active');
|
2026-09-18 08:54:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('大小写敏感(bin 校对):“AL”/“carol” → 空;“Carol” → 命中', async () => {
|
|
|
|
|
|
const upper = await lookup(staffToken, { accountPrefix: 'AL' });
|
|
|
|
|
|
expect(upper.status).toBe(200);
|
|
|
|
|
|
expect(upper.body.items).toEqual([]);
|
|
|
|
|
|
|
|
|
|
|
|
const lowerCarol = await lookup(staffToken, { accountPrefix: 'carol' });
|
|
|
|
|
|
expect(lowerCarol.status).toBe(200);
|
|
|
|
|
|
expect(lowerCarol.body.items).toEqual([]);
|
|
|
|
|
|
|
|
|
|
|
|
const exact = await lookup(staffToken, { accountPrefix: 'Carol' });
|
|
|
|
|
|
expect(exact.status).toBe(200);
|
2026-09-20 15:56:38 +08:00
|
|
|
|
// 条目为 AdminUser 全字段:以关键字段子集断言(字段全集见用例 ①)。
|
2026-09-18 08:54:44 +08:00
|
|
|
|
expect(exact.body.items).toEqual([
|
2026-09-20 15:56:38 +08:00
|
|
|
|
expect.objectContaining({
|
|
|
|
|
|
id: carolId,
|
|
|
|
|
|
account: 'Carol',
|
|
|
|
|
|
displayName: '卡萝',
|
|
|
|
|
|
}),
|
2026-09-18 08:54:44 +08:00
|
|
|
|
]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('LIKE 通配符按字面匹配(ESCAPE):“esc_” 仅命中字面下划线账号;“al%” 空', async () => {
|
|
|
|
|
|
// 未转义时 _ 作单字符通配会同时命中 escshape——转义后只命中 esc_ape。
|
|
|
|
|
|
const underscore = await lookup(staffToken, { accountPrefix: 'esc_' });
|
|
|
|
|
|
expect(underscore.status).toBe(200);
|
|
|
|
|
|
expect(underscore.body.items).toEqual([
|
2026-09-20 15:56:38 +08:00
|
|
|
|
expect.objectContaining({
|
|
|
|
|
|
id: escUnderscoreId,
|
|
|
|
|
|
account: 'esc_ape',
|
|
|
|
|
|
displayName: '下划线',
|
|
|
|
|
|
}),
|
2026-09-18 08:54:44 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 未转义时 al% 作任意串通配会命中 albert/alice 等——转义后按字面前缀匹配。
|
|
|
|
|
|
const percent = await lookup(staffToken, { accountPrefix: 'al%' });
|
|
|
|
|
|
expect(percent.status).toBe(200);
|
|
|
|
|
|
expect(percent.body.items).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-20 19:31:00 +08:00
|
|
|
|
it('accountPrefix 边界:缺失 → 双模式分流(staff 403 / admin 200 管理列表);1/65 字符 → 400;64 → 200 空', async () => {
|
|
|
|
|
|
// Task 6 起缺失 accountPrefix 进入管理列表模式(仅 admin):staff 无前缀
|
|
|
|
|
|
// → 403 AUTH_FORBIDDEN;admin → 200 管理列表(全量用户,含非激活态——
|
|
|
|
|
|
// 深度断言由 admin-users.e2e-spec 守护,此处只断言模式分流与响应形状)。
|
|
|
|
|
|
const staffMissing = await lookup(staffToken, {});
|
|
|
|
|
|
expect(staffMissing.status).toBe(403);
|
|
|
|
|
|
expect(staffMissing.body.code).toBe(ErrorCodes.AUTH_FORBIDDEN);
|
|
|
|
|
|
|
|
|
|
|
|
const adminMissing = await lookup(adminToken, {});
|
|
|
|
|
|
expect(adminMissing.status).toBe(200);
|
|
|
|
|
|
expect(Object.keys(adminMissing.body).sort()).toEqual([
|
|
|
|
|
|
'hasMore',
|
|
|
|
|
|
'items',
|
|
|
|
|
|
'nextCursor',
|
2026-09-22 18:06:12 +08:00
|
|
|
|
'total',
|
2026-09-20 19:31:00 +08:00
|
|
|
|
]);
|
|
|
|
|
|
// 管理列表包含前缀模式排除的非激活用户(本 spec 的排除组)。
|
|
|
|
|
|
const adminAccounts = (
|
|
|
|
|
|
adminMissing.body.items as { account: string }[]
|
|
|
|
|
|
).map((item) => item.account);
|
|
|
|
|
|
expect(adminAccounts).toContain('aldis');
|
|
|
|
|
|
expect(adminAccounts).toContain('alpend');
|
|
|
|
|
|
expect(adminAccounts).toContain('allock');
|
|
|
|
|
|
|
|
|
|
|
|
// 显式空串是「提供了非法值」而非「未提供」→ 仍 400。
|
2026-09-18 08:54:44 +08:00
|
|
|
|
const empty = await lookup(staffToken, { accountPrefix: '' });
|
|
|
|
|
|
expect(empty.status).toBe(400);
|
|
|
|
|
|
expect(empty.body.code).toBe(ErrorCodes.VALIDATION_FAILED);
|
|
|
|
|
|
|
|
|
|
|
|
const short = await lookup(staffToken, { accountPrefix: 'a' });
|
|
|
|
|
|
expect(short.status).toBe(400);
|
|
|
|
|
|
expect(short.body.code).toBe(ErrorCodes.VALIDATION_FAILED);
|
|
|
|
|
|
|
|
|
|
|
|
const long = await lookup(staffToken, { accountPrefix: 'a'.repeat(65) });
|
|
|
|
|
|
expect(long.status).toBe(400);
|
|
|
|
|
|
expect(long.body.code).toBe(ErrorCodes.VALIDATION_FAILED);
|
|
|
|
|
|
|
|
|
|
|
|
const max = await lookup(staffToken, { accountPrefix: 'a'.repeat(64) });
|
|
|
|
|
|
expect(max.status).toBe(200);
|
|
|
|
|
|
expect(max.body.items).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-20 15:56:38 +08:00
|
|
|
|
it('limit:缺省 20(P4a 统一 bounds);5 合法;0/101/非整数 → 400;100 合法', async () => {
|
|
|
|
|
|
// zz-look-* 共 12 个激活账号:缺省 limit=20 > 存量 12 → 全量升序返回。
|
2026-09-18 08:54:44 +08:00
|
|
|
|
const def = await lookup(staffToken, { accountPrefix: 'zz-look-' });
|
|
|
|
|
|
expect(def.status).toBe(200);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
(def.body.items as { account: string }[]).map((item) => item.account),
|
2026-09-20 15:56:38 +08:00
|
|
|
|
).toEqual(Array.from({ length: 12 }, (_, i) => `zz-look-${String(i + 1).padStart(2, '0')}`));
|
2026-09-18 08:54:44 +08:00
|
|
|
|
|
|
|
|
|
|
const five = await lookup(staffToken, {
|
|
|
|
|
|
accountPrefix: 'zz-look-',
|
|
|
|
|
|
limit: 5,
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(five.status).toBe(200);
|
|
|
|
|
|
expect(five.body.items).toHaveLength(5);
|
|
|
|
|
|
|
|
|
|
|
|
const top = await lookup(staffToken, {
|
|
|
|
|
|
accountPrefix: 'zz-look-',
|
2026-09-20 15:56:38 +08:00
|
|
|
|
limit: 100,
|
2026-09-18 08:54:44 +08:00
|
|
|
|
});
|
|
|
|
|
|
expect(top.status).toBe(200);
|
2026-09-20 15:56:38 +08:00
|
|
|
|
expect(top.body.items).toHaveLength(12); // 上限 100 > 存量 12,全量返回
|
2026-09-18 08:54:44 +08:00
|
|
|
|
|
2026-09-20 15:56:38 +08:00
|
|
|
|
for (const limit of [0, 101, 'abc']) {
|
2026-09-18 08:54:44 +08:00
|
|
|
|
const res = await lookup(staffToken, {
|
|
|
|
|
|
accountPrefix: 'zz-look-',
|
|
|
|
|
|
limit,
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(res.status).toBe(400);
|
|
|
|
|
|
expect(res.body.code).toBe(ErrorCodes.VALIDATION_FAILED);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('访问策略:未认证 401;已认证 staff(非 admin)200;admin 200', async () => {
|
|
|
|
|
|
const anon = await lookup(null, { accountPrefix: 'al' });
|
|
|
|
|
|
expect(anon.status).toBe(401);
|
|
|
|
|
|
expect(anon.body.code).toBe(ErrorCodes.AUTH_TOKEN_EXPIRED);
|
|
|
|
|
|
|
|
|
|
|
|
// P1b 决策(契约描述):邀请发起方不限于 admin/owner,任何已认证工作人员
|
|
|
|
|
|
// 可用——此处显式断言非 admin 的 staff 可 200。
|
|
|
|
|
|
const staff = await lookup(staffToken, { accountPrefix: 'al' });
|
|
|
|
|
|
expect(staff.status).toBe(200);
|
|
|
|
|
|
expect(staff.body.items).toHaveLength(2);
|
|
|
|
|
|
|
|
|
|
|
|
const admin = await lookup(adminToken, { accountPrefix: 'al' });
|
|
|
|
|
|
expect(admin.status).toBe(200);
|
|
|
|
|
|
expect(admin.body.items).toHaveLength(2);
|
|
|
|
|
|
});
|
2026-09-22 18:06:12 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
2026-09-18 08:54:44 +08:00
|
|
|
|
});
|