|
|
@@ -4,11 +4,10 @@ import {
|
|
|
IntegrationTestDatabase,
|
|
|
setupIntegrationDatabaseHooksWithEntities,
|
|
|
} from '@d8d/shared-test-util';
|
|
|
-import { RoleMt, UserEntityMt } from '@d8d/user-module-mt';
|
|
|
+import { RoleMt, UserEntityMt, UserServiceMt } from '@d8d/user-module-mt';
|
|
|
import { FileMt } from '@d8d/file-module-mt';
|
|
|
import authRoutes from '../../src/routes';
|
|
|
import { AuthService } from '../../src/services';
|
|
|
-import { UserService } from '@d8d/user-module-mt';
|
|
|
import { DisabledStatus } from '@d8d/shared-types';
|
|
|
import { TestDataFactory } from '../utils/test-data-factory';
|
|
|
|
|
|
@@ -18,7 +17,7 @@ setupIntegrationDatabaseHooksWithEntities([UserEntityMt, RoleMt, FileMt])
|
|
|
describe('认证API集成测试 (使用hono/testing)', () => {
|
|
|
let client: ReturnType<typeof testClient<typeof authRoutes>>;
|
|
|
let authService: AuthService;
|
|
|
- let userService: UserService;
|
|
|
+ let userService: UserServiceMt;
|
|
|
let testToken: string;
|
|
|
let testUser: any;
|
|
|
|
|
|
@@ -29,8 +28,13 @@ describe('认证API集成测试 (使用hono/testing)', () => {
|
|
|
// 获取数据源
|
|
|
const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
|
|
|
+ // 确保数据源已初始化
|
|
|
+ if (!dataSource.isInitialized) {
|
|
|
+ await dataSource.initialize();
|
|
|
+ }
|
|
|
+
|
|
|
// 初始化服务
|
|
|
- userService = new UserService(dataSource);
|
|
|
+ userService = new UserServiceMt(dataSource);
|
|
|
authService = new AuthService(userService);
|
|
|
|
|
|
// 创建测试用户前先删除可能存在的重复用户
|
|
|
@@ -112,7 +116,7 @@ describe('认证API集成测试 (使用hono/testing)', () => {
|
|
|
if (!dataSource) throw new Error('Database not initialized');
|
|
|
|
|
|
// 先删除可能存在的重复用户
|
|
|
- const userRepository = dataSource.getRepository(UserEntity);
|
|
|
+ const userRepository = dataSource.getRepository(UserEntityMt);
|
|
|
await userRepository.delete({ username: 'disabled_user' });
|
|
|
|
|
|
await TestDataFactory.createTestUser(dataSource, {
|
|
|
@@ -348,7 +352,7 @@ describe('认证API集成测试 (使用hono/testing)', () => {
|
|
|
if (!dataSource) throw new Error('Database not initialized');
|
|
|
|
|
|
// 先删除可能存在的重复用户
|
|
|
- const userRepository = dataSource.getRepository(UserEntity);
|
|
|
+ const userRepository = dataSource.getRepository(UserEntityMt);
|
|
|
await userRepository.delete({ username: 'regular_user' });
|
|
|
|
|
|
const regularUser = await TestDataFactory.createTestUser(dataSource, {
|
|
|
@@ -409,4 +413,258 @@ describe('认证API集成测试 (使用hono/testing)', () => {
|
|
|
expect(responseTime).toBeLessThan(200); // 响应时间应小于200ms
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+ describe('租户隔离测试', () => {
|
|
|
+ it('应该成功注册不同租户的用户', async () => {
|
|
|
+ // 租户1的用户注册
|
|
|
+ const tenant1Response = await client.register.$post({
|
|
|
+ json: {
|
|
|
+ username: 'tenant1_user',
|
|
|
+ password: 'password123',
|
|
|
+ email: 'tenant1@example.com'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(tenant1Response.status).toBe(201);
|
|
|
+ if (tenant1Response.status === 201) {
|
|
|
+ const tenant1Data = await tenant1Response.json();
|
|
|
+ expect(tenant1Data.token).toBeDefined();
|
|
|
+ expect(tenant1Data.user.username).toBe('tenant1_user');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 租户2的用户注册
|
|
|
+ const tenant2Response = await client.register.$post({
|
|
|
+ json: {
|
|
|
+ username: 'tenant2_user',
|
|
|
+ password: 'password123',
|
|
|
+ email: 'tenant2@example.com'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '2'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(tenant2Response.status).toBe(201);
|
|
|
+ if (tenant2Response.status === 201) {
|
|
|
+ const tenant2Data = await tenant2Response.json();
|
|
|
+ expect(tenant2Data.token).toBeDefined();
|
|
|
+ expect(tenant2Data.user.username).toBe('tenant2_user');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该成功登录到正确的租户', async () => {
|
|
|
+ // 先注册租户1的用户
|
|
|
+ await client.register.$post({
|
|
|
+ json: {
|
|
|
+ username: 'login_tenant1',
|
|
|
+ password: 'password123',
|
|
|
+ email: 'login1@example.com'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 租户1的用户登录
|
|
|
+ const tenant1Response = await client.login.$post({
|
|
|
+ json: {
|
|
|
+ username: 'login_tenant1',
|
|
|
+ password: 'password123'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(tenant1Response.status).toBe(200);
|
|
|
+ if (tenant1Response.status === 200) {
|
|
|
+ const tenant1Data = await tenant1Response.json();
|
|
|
+ expect(tenant1Data.token).toBeDefined();
|
|
|
+ expect(tenant1Data.user.username).toBe('login_tenant1');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先注册租户2的用户
|
|
|
+ await client.register.$post({
|
|
|
+ json: {
|
|
|
+ username: 'login_tenant2',
|
|
|
+ password: 'password123',
|
|
|
+ email: 'login2@example.com'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '2'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 租户2的用户登录
|
|
|
+ const tenant2Response = await client.login.$post({
|
|
|
+ json: {
|
|
|
+ username: 'login_tenant2',
|
|
|
+ password: 'password123'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '2'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(tenant2Response.status).toBe(200);
|
|
|
+ if (tenant2Response.status === 200) {
|
|
|
+ const tenant2Data = await tenant2Response.json();
|
|
|
+ expect(tenant2Data.token).toBeDefined();
|
|
|
+ expect(tenant2Data.user.username).toBe('login_tenant2');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该拒绝跨租户登录', async () => {
|
|
|
+ // 先注册租户1的用户
|
|
|
+ await client.register.$post({
|
|
|
+ json: {
|
|
|
+ username: 'cross_tenant_user',
|
|
|
+ password: 'password123',
|
|
|
+ email: 'cross@example.com'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 尝试用租户1的用户登录到租户2
|
|
|
+ const crossTenantResponse = await client.login.$post({
|
|
|
+ json: {
|
|
|
+ username: 'cross_tenant_user',
|
|
|
+ password: 'password123'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '2'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(crossTenantResponse.status).toBe(401);
|
|
|
+ if (crossTenantResponse.status === 401) {
|
|
|
+ const errorData = await crossTenantResponse.json();
|
|
|
+ expect(errorData.message).toBe('用户名或密码错误');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该允许无租户ID的登录(向后兼容)', async () => {
|
|
|
+ // 创建无租户的用户
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ // 先删除可能存在的重复用户
|
|
|
+ const userRepository = dataSource.getRepository(UserEntityMt);
|
|
|
+ await userRepository.delete({ username: 'notenant' });
|
|
|
+
|
|
|
+ await TestDataFactory.createTestUser(dataSource, {
|
|
|
+ username: 'notenant',
|
|
|
+ password: 'password123',
|
|
|
+ email: 'notenant@example.com'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 无租户ID登录
|
|
|
+ const response = await client.login.$post({
|
|
|
+ json: {
|
|
|
+ username: 'notenant',
|
|
|
+ password: 'password123'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const data = await response.json();
|
|
|
+ expect(data.token).toBeDefined();
|
|
|
+ expect(data.user.username).toBe('notenant');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该在认证后设置租户上下文', async () => {
|
|
|
+ // 先注册用户
|
|
|
+ await client.register.$post({
|
|
|
+ json: {
|
|
|
+ username: 'context_user',
|
|
|
+ password: 'password123',
|
|
|
+ email: 'context@example.com'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 先登录获取token
|
|
|
+ const loginResponse = await client.login.$post({
|
|
|
+ json: {
|
|
|
+ username: 'context_user',
|
|
|
+ password: 'password123'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const loginData = await loginResponse.json();
|
|
|
+ const token = loginData.token;
|
|
|
+
|
|
|
+ // 使用token访问需要认证的端点
|
|
|
+ const meResponse = await client.me.$get(
|
|
|
+ {},
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(meResponse.status).toBe(200);
|
|
|
+ if (meResponse.status === 200) {
|
|
|
+ const meData = await meResponse.json();
|
|
|
+ expect(meData.tenantId).toBe(1); // 应该包含租户ID
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该在JWT token中包含租户ID', async () => {
|
|
|
+ // 先注册用户
|
|
|
+ await client.register.$post({
|
|
|
+ json: {
|
|
|
+ username: 'jwt_user',
|
|
|
+ password: 'password123',
|
|
|
+ email: 'jwt@example.com'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const response = await client.login.$post({
|
|
|
+ json: {
|
|
|
+ username: 'jwt_user',
|
|
|
+ password: 'password123'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'X-Tenant-Id': '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ const token = data.token;
|
|
|
+
|
|
|
+ // 解码token验证租户ID
|
|
|
+ const { JWTUtil } = await import('@d8d/shared-utils');
|
|
|
+ const decoded = JWTUtil.decodeToken(token);
|
|
|
+ expect(decoded?.tenantId).toBe(1);
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|