412 lines
13 KiB
TypeScript
412 lines
13 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||
import * as request from 'supertest';
|
||
import { AppModule } from '@/app.module';
|
||
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
||
|
||
describe('Identity Service E2E Tests', () => {
|
||
let app: INestApplication;
|
||
let prisma: PrismaService;
|
||
let accessToken: string;
|
||
let refreshToken: string;
|
||
let accountSequence: number;
|
||
let mnemonic: string;
|
||
let referralCode: string;
|
||
let deviceId: string;
|
||
|
||
beforeAll(async () => {
|
||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||
imports: [AppModule],
|
||
}).compile();
|
||
|
||
app = moduleFixture.createNestApplication();
|
||
|
||
// 设置全局前缀,与 main.ts 保持一致
|
||
app.setGlobalPrefix('api/v1');
|
||
|
||
// 设置全局验证管道
|
||
app.useGlobalPipes(
|
||
new ValidationPipe({
|
||
whitelist: true,
|
||
forbidNonWhitelisted: true,
|
||
transform: true,
|
||
transformOptions: { enableImplicitConversion: true },
|
||
}),
|
||
);
|
||
|
||
prisma = app.get<PrismaService>(PrismaService);
|
||
|
||
await app.init();
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await app.close();
|
||
});
|
||
|
||
beforeEach(() => {
|
||
deviceId = `test-device-${Date.now()}`;
|
||
});
|
||
|
||
describe('1. 用户注册和账户创建', () => {
|
||
it('应该成功自动创建账户', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.post('/api/v1/user/auto-create')
|
||
.send({
|
||
deviceId,
|
||
deviceName: 'Test Device',
|
||
provinceCode: '110000',
|
||
cityCode: '110100',
|
||
})
|
||
.expect(201);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.data).toHaveProperty('userId');
|
||
expect(response.body.data).toHaveProperty('accountSequence');
|
||
expect(response.body.data).toHaveProperty('referralCode');
|
||
expect(response.body.data).toHaveProperty('mnemonic');
|
||
expect(response.body.data).toHaveProperty('walletAddresses');
|
||
expect(response.body.data).toHaveProperty('accessToken');
|
||
expect(response.body.data).toHaveProperty('refreshToken');
|
||
|
||
// 保存数据供后续测试使用
|
||
accessToken = response.body.data.accessToken;
|
||
refreshToken = response.body.data.refreshToken;
|
||
accountSequence = response.body.data.accountSequence;
|
||
mnemonic = response.body.data.mnemonic;
|
||
referralCode = response.body.data.referralCode;
|
||
|
||
// 验证钱包地址
|
||
expect(response.body.data.walletAddresses).toHaveProperty('kava');
|
||
expect(response.body.data.walletAddresses).toHaveProperty('dst');
|
||
expect(response.body.data.walletAddresses).toHaveProperty('bsc');
|
||
expect(response.body.data.walletAddresses.kava).toMatch(/^kava1[a-z0-9]{38}$/);
|
||
expect(response.body.data.walletAddresses.dst).toMatch(/^dst1[a-z0-9]{38}$/);
|
||
expect(response.body.data.walletAddresses.bsc).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
||
});
|
||
|
||
it('应该验证请求参数', async () => {
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/auto-create')
|
||
.send({
|
||
// 缺少必需字段
|
||
deviceName: 'Test Device',
|
||
})
|
||
.expect(400);
|
||
});
|
||
});
|
||
|
||
describe('2. 用户资料管理', () => {
|
||
it('应该获取个人资料', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.get('/api/v1/user/my-profile')
|
||
.set('Authorization', `Bearer ${accessToken}`)
|
||
.expect(200);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.data).toHaveProperty('userId');
|
||
expect(response.body.data).toHaveProperty('accountSequence');
|
||
expect(response.body.data.accountSequence).toBe(accountSequence);
|
||
});
|
||
|
||
it('应该拒绝未认证的请求', async () => {
|
||
await request(app.getHttpServer())
|
||
.get('/api/v1/user/my-profile')
|
||
.expect(401);
|
||
});
|
||
|
||
it('应该更新个人资料', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.put('/api/v1/user/update-profile')
|
||
.set('Authorization', `Bearer ${accessToken}`)
|
||
.send({
|
||
nickname: '测试用户',
|
||
avatarUrl: 'https://example.com/avatar.jpg',
|
||
address: '测试地址',
|
||
})
|
||
.expect(200);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.data.nickname).toBe('测试用户');
|
||
});
|
||
});
|
||
|
||
describe('3. 设备管理', () => {
|
||
it('应该获取设备列表', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.get('/api/v1/user/my-devices')
|
||
.set('Authorization', `Bearer ${accessToken}`)
|
||
.expect(200);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(Array.isArray(response.body.data)).toBe(true);
|
||
expect(response.body.data.length).toBeGreaterThan(0);
|
||
expect(response.body.data[0]).toHaveProperty('deviceId');
|
||
expect(response.body.data[0]).toHaveProperty('deviceName');
|
||
});
|
||
|
||
it('应该添加新设备(通过助记词恢复)', async () => {
|
||
const newDeviceId = `test-device-new-${Date.now()}`;
|
||
|
||
const response = await request(app.getHttpServer())
|
||
.post('/api/v1/user/recover-by-mnemonic')
|
||
.send({
|
||
accountSequence,
|
||
mnemonic,
|
||
deviceId: newDeviceId,
|
||
deviceName: '新设备',
|
||
})
|
||
.expect(201);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.data).toHaveProperty('accessToken');
|
||
|
||
// 验证设备已添加
|
||
const newAccessToken = response.body.data.accessToken;
|
||
const devicesResponse = await request(app.getHttpServer())
|
||
.get('/api/v1/user/my-devices')
|
||
.set('Authorization', `Bearer ${newAccessToken}`)
|
||
.expect(200);
|
||
|
||
expect(devicesResponse.body.data.length).toBe(2);
|
||
});
|
||
|
||
it('应该移除设备', async () => {
|
||
const newDeviceId = `test-device-remove-${Date.now()}`;
|
||
|
||
// 先添加设备
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/recover-by-mnemonic')
|
||
.send({
|
||
accountSequence,
|
||
mnemonic,
|
||
deviceId: newDeviceId,
|
||
deviceName: '待删除设备',
|
||
})
|
||
.expect(201);
|
||
|
||
// 移除设备
|
||
const response = await request(app.getHttpServer())
|
||
.post('/api/v1/user/remove-device')
|
||
.set('Authorization', `Bearer ${accessToken}`)
|
||
.send({
|
||
deviceId: newDeviceId,
|
||
})
|
||
.expect(201);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
});
|
||
|
||
it('应该限制设备数量为5个', async () => {
|
||
// 添加5个设备
|
||
for (let i = 0; i < 5; i++) {
|
||
const testDeviceId = `test-device-limit-${Date.now()}-${i}`;
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/recover-by-mnemonic')
|
||
.send({
|
||
accountSequence,
|
||
mnemonic,
|
||
deviceId: testDeviceId,
|
||
deviceName: `设备${i + 1}`,
|
||
})
|
||
.expect(201);
|
||
}
|
||
|
||
// 尝试添加第6个设备,应该失败
|
||
const sixthDeviceId = `test-device-sixth-${Date.now()}`;
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/recover-by-mnemonic')
|
||
.send({
|
||
accountSequence,
|
||
mnemonic,
|
||
deviceId: sixthDeviceId,
|
||
deviceName: '第6个设备',
|
||
})
|
||
.expect(400);
|
||
});
|
||
});
|
||
|
||
describe('4. Token 管理', () => {
|
||
it('应该使用 refresh token 获取新的 access token', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.post('/api/v1/user/auto-login')
|
||
.send({
|
||
refreshToken,
|
||
deviceId,
|
||
})
|
||
.expect(201);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.data).toHaveProperty('accessToken');
|
||
expect(response.body.data.accessToken).not.toBe(accessToken);
|
||
});
|
||
|
||
it('应该拒绝无效的 refresh token', async () => {
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/auto-login')
|
||
.send({
|
||
refreshToken: 'invalid-token',
|
||
deviceId,
|
||
})
|
||
.expect(401);
|
||
});
|
||
});
|
||
|
||
describe('5. 推荐系统', () => {
|
||
it('应该根据推荐码查询用户', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.get(`/api/v1/user/by-referral-code/${referralCode}`)
|
||
.expect(200);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.data).toHaveProperty('accountSequence');
|
||
expect(response.body.data.accountSequence).toBe(accountSequence);
|
||
});
|
||
|
||
it('应该在注册时使用推荐码', async () => {
|
||
// 创建第一个用户
|
||
const inviter = await request(app.getHttpServer())
|
||
.post('/api/v1/user/auto-create')
|
||
.send({
|
||
deviceId: `inviter-device-${Date.now()}`,
|
||
deviceName: 'Inviter Device',
|
||
provinceCode: '110000',
|
||
cityCode: '110100',
|
||
})
|
||
.expect(201);
|
||
|
||
const inviterReferralCode = inviter.body.data.referralCode;
|
||
|
||
// 创建第二个用户,使用第一个用户的推荐码
|
||
const invitee = await request(app.getHttpServer())
|
||
.post('/api/v1/user/auto-create')
|
||
.send({
|
||
deviceId: `invitee-device-${Date.now()}`,
|
||
deviceName: 'Invitee Device',
|
||
provinceCode: '110000',
|
||
cityCode: '110100',
|
||
inviterReferralCode,
|
||
})
|
||
.expect(201);
|
||
|
||
// 验证邀请关系
|
||
const inviteeProfile = await request(app.getHttpServer())
|
||
.get('/api/v1/user/my-profile')
|
||
.set('Authorization', `Bearer ${invitee.body.data.accessToken}`)
|
||
.expect(200);
|
||
|
||
// 注意:这里需要根据你的实际实现调整字段名
|
||
// expect(inviteeProfile.body.data).toHaveProperty('inviterSequence');
|
||
});
|
||
});
|
||
|
||
describe('6. KYC 认证', () => {
|
||
it('应该提交 KYC 认证', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.post('/api/v1/user/submit-kyc')
|
||
.set('Authorization', `Bearer ${accessToken}`)
|
||
.send({
|
||
realName: '张三',
|
||
idCardNumber: '110101199001011234',
|
||
idCardFrontUrl: 'https://example.com/id-front.jpg',
|
||
idCardBackUrl: 'https://example.com/id-back.jpg',
|
||
})
|
||
.expect(201);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
});
|
||
|
||
it('应该验证身份证号格式', async () => {
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/submit-kyc')
|
||
.set('Authorization', `Bearer ${accessToken}`)
|
||
.send({
|
||
realName: '张三',
|
||
idCardNumber: 'invalid-id',
|
||
idCardFrontUrl: 'https://example.com/id-front.jpg',
|
||
idCardBackUrl: 'https://example.com/id-back.jpg',
|
||
})
|
||
.expect(400);
|
||
});
|
||
});
|
||
|
||
describe('7. 助记词恢复', () => {
|
||
it('应该使用正确的助记词恢复账户', async () => {
|
||
const newDeviceId = `recovery-device-${Date.now()}`;
|
||
|
||
const response = await request(app.getHttpServer())
|
||
.post('/api/v1/user/recover-by-mnemonic')
|
||
.send({
|
||
accountSequence,
|
||
mnemonic,
|
||
deviceId: newDeviceId,
|
||
deviceName: '恢复设备',
|
||
})
|
||
.expect(201);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.data).toHaveProperty('userId');
|
||
expect(response.body.data).toHaveProperty('accessToken');
|
||
});
|
||
|
||
it('应该拒绝错误的助记词', async () => {
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/recover-by-mnemonic')
|
||
.send({
|
||
accountSequence,
|
||
mnemonic: 'wrong wrong wrong wrong wrong wrong wrong wrong wrong wrong wrong wrong',
|
||
deviceId: `wrong-device-${Date.now()}`,
|
||
deviceName: '错误设备',
|
||
})
|
||
.expect(400);
|
||
});
|
||
|
||
it('应该拒绝不匹配的账户序列号', async () => {
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/recover-by-mnemonic')
|
||
.send({
|
||
accountSequence: 999999,
|
||
mnemonic,
|
||
deviceId: `mismatch-device-${Date.now()}`,
|
||
deviceName: '不匹配设备',
|
||
})
|
||
.expect(404);
|
||
});
|
||
});
|
||
|
||
describe('8. 数据验证', () => {
|
||
it('应该验证手机号格式', async () => {
|
||
await request(app.getHttpServer())
|
||
.post('/api/v1/user/bind-phone')
|
||
.set('Authorization', `Bearer ${accessToken}`)
|
||
.send({
|
||
phoneNumber: 'invalid-phone',
|
||
smsCode: '123456',
|
||
})
|
||
.expect(400);
|
||
});
|
||
|
||
it('应该接受有效的手机号格式', async () => {
|
||
const validPhones = [
|
||
'13800138000',
|
||
'13912345678',
|
||
'15800001111',
|
||
'18600002222',
|
||
];
|
||
|
||
for (const phone of validPhones) {
|
||
// 注意:这里会因为验证码不存在而失败,但至少验证了格式通过
|
||
const response = await request(app.getHttpServer())
|
||
.post('/api/v1/user/bind-phone')
|
||
.set('Authorization', `Bearer ${accessToken}`)
|
||
.send({
|
||
phoneNumber: phone,
|
||
smsCode: '123456',
|
||
});
|
||
|
||
// 不应该因为格式错误而返回 400
|
||
expect(response.status).not.toBe(400);
|
||
}
|
||
});
|
||
});
|
||
});
|