rwadurian/backend/services/identity-service/test/app.e2e-spec.ts

412 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
});
});
});