|
|
@@ -0,0 +1,303 @@
|
|
|
+import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
+import { testClient } from 'hono/testing';
|
|
|
+import {
|
|
|
+ IntegrationTestDatabase,
|
|
|
+ setupIntegrationDatabaseHooksWithEntities
|
|
|
+} from '@d8d/shared-test-util';
|
|
|
+import { systemConfigRoutesMt } from '../../src/routes/system-config.routes.mt';
|
|
|
+import { SystemConfigMt } from '../../src/entities/system-config.entity.mt';
|
|
|
+import { SystemConfigServiceMt } from '../../src/services/system-config.service.mt';
|
|
|
+import { UserEntityMt, RoleMt } from '@d8d/core-module-mt/user-module-mt';
|
|
|
+import { FileMt } from '@d8d/core-module-mt/file-module-mt';
|
|
|
+import { TestDataFactory } from '../utils/integration-test-db';
|
|
|
+import { AuthService } from '@d8d/core-module-mt/auth-module-mt';
|
|
|
+import { UserServiceMt } from '@d8d/core-module-mt/user-module-mt';
|
|
|
+import { redisUtil } from '@d8d/shared-utils';
|
|
|
+
|
|
|
+// 设置集成测试钩子
|
|
|
+setupIntegrationDatabaseHooksWithEntities([SystemConfigMt, UserEntityMt, RoleMt, FileMt])
|
|
|
+
|
|
|
+describe('系统配置Redis缓存集成测试', () => {
|
|
|
+ let client: ReturnType<typeof testClient<typeof systemConfigRoutesMt>>;
|
|
|
+ let authService: AuthService;
|
|
|
+ let userService: UserServiceMt;
|
|
|
+ let systemConfigService: SystemConfigServiceMt;
|
|
|
+ let testToken: string;
|
|
|
+ let testUser: any;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ // 创建测试客户端
|
|
|
+ client = testClient(systemConfigRoutesMt);
|
|
|
+
|
|
|
+ // 获取数据源
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ // 初始化服务
|
|
|
+ userService = new UserServiceMt(dataSource);
|
|
|
+ authService = new AuthService(userService);
|
|
|
+ systemConfigService = new SystemConfigServiceMt(dataSource);
|
|
|
+
|
|
|
+ // 创建测试用户并生成token
|
|
|
+ testUser = await TestDataFactory.createTestUser(dataSource, {
|
|
|
+ username: 'testuser_rediscache',
|
|
|
+ password: 'TestPassword123!',
|
|
|
+ email: 'testuser_rediscache@example.com'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 生成测试用户的token
|
|
|
+ testToken = authService.generateToken(testUser);
|
|
|
+
|
|
|
+ // 清除测试前的缓存
|
|
|
+ await redisUtil.clearTenantSystemConfigs(testUser.tenantId);
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('缓存命中测试', () => {
|
|
|
+ it('应该从缓存中获取配置值', async () => {
|
|
|
+ // 先创建配置
|
|
|
+ const config = await systemConfigService.create({
|
|
|
+ configKey: 'app.cache.test',
|
|
|
+ configValue: 'cached-value',
|
|
|
+ description: '缓存测试配置',
|
|
|
+ tenantId: testUser.tenantId
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ // 第一次查询 - 应该从数据库获取并写入缓存
|
|
|
+ const firstResult = await systemConfigService.getConfigByKey('app.cache.test', testUser.tenantId);
|
|
|
+ expect(firstResult).toBe('cached-value');
|
|
|
+
|
|
|
+ // 验证缓存已写入
|
|
|
+ const cachedValue = await redisUtil.getSystemConfig(testUser.tenantId, 'app.cache.test');
|
|
|
+ expect(cachedValue).toBe('cached-value');
|
|
|
+
|
|
|
+ // 第二次查询 - 应该从缓存获取
|
|
|
+ const secondResult = await systemConfigService.getConfigByKey('app.cache.test', testUser.tenantId);
|
|
|
+ expect(secondResult).toBe('cached-value');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该批量从缓存中获取配置值', async () => {
|
|
|
+ // 创建多个配置
|
|
|
+ await systemConfigService.create({
|
|
|
+ configKey: 'app.feature1.enabled',
|
|
|
+ configValue: 'true',
|
|
|
+ tenantId: testUser.tenantId
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ await systemConfigService.create({
|
|
|
+ configKey: 'app.feature2.enabled',
|
|
|
+ configValue: 'false',
|
|
|
+ tenantId: testUser.tenantId
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ // 第一次批量查询 - 应该从数据库获取并写入缓存
|
|
|
+ const firstResult = await systemConfigService.getConfigsByKeys(
|
|
|
+ ['app.feature1.enabled', 'app.feature2.enabled'],
|
|
|
+ testUser.tenantId
|
|
|
+ );
|
|
|
+ expect(firstResult['app.feature1.enabled']).toBe('true');
|
|
|
+ expect(firstResult['app.feature2.enabled']).toBe('false');
|
|
|
+
|
|
|
+ // 验证缓存已写入
|
|
|
+ const cachedValues = await redisUtil.getSystemConfigs(testUser.tenantId, ['app.feature1.enabled', 'app.feature2.enabled']);
|
|
|
+ expect(cachedValues['app.feature1.enabled']).toBe('true');
|
|
|
+ expect(cachedValues['app.feature2.enabled']).toBe('false');
|
|
|
+
|
|
|
+ // 第二次批量查询 - 应该从缓存获取
|
|
|
+ const secondResult = await systemConfigService.getConfigsByKeys(
|
|
|
+ ['app.feature1.enabled', 'app.feature2.enabled'],
|
|
|
+ testUser.tenantId
|
|
|
+ );
|
|
|
+ expect(secondResult['app.feature1.enabled']).toBe('true');
|
|
|
+ expect(secondResult['app.feature2.enabled']).toBe('false');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('缓存失效测试', () => {
|
|
|
+ it('应该在配置更新时清除缓存', async () => {
|
|
|
+ // 创建配置
|
|
|
+ const config = await systemConfigService.create({
|
|
|
+ configKey: 'app.update.test',
|
|
|
+ configValue: 'initial-value',
|
|
|
+ tenantId: testUser.tenantId
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ // 查询一次以填充缓存
|
|
|
+ await systemConfigService.getConfigByKey('app.update.test', testUser.tenantId);
|
|
|
+
|
|
|
+ // 验证缓存已写入
|
|
|
+ let cachedValue = await redisUtil.getSystemConfig(testUser.tenantId, 'app.update.test');
|
|
|
+ expect(cachedValue).toBe('initial-value');
|
|
|
+
|
|
|
+ // 更新配置
|
|
|
+ await systemConfigService.setConfig('app.update.test', 'updated-value', testUser.tenantId, '更新后的描述');
|
|
|
+
|
|
|
+ // 验证缓存已清除
|
|
|
+ cachedValue = await redisUtil.getSystemConfig(testUser.tenantId, 'app.update.test');
|
|
|
+ expect(cachedValue).toBeNull();
|
|
|
+
|
|
|
+ // 再次查询应该从数据库获取新值
|
|
|
+ const result = await systemConfigService.getConfigByKey('app.update.test', testUser.tenantId);
|
|
|
+ expect(result).toBe('updated-value');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该在配置删除时清除缓存', async () => {
|
|
|
+ // 创建配置
|
|
|
+ const config = await systemConfigService.create({
|
|
|
+ configKey: 'app.delete.test',
|
|
|
+ configValue: 'to-be-deleted',
|
|
|
+ tenantId: testUser.tenantId
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ // 查询一次以填充缓存
|
|
|
+ await systemConfigService.getConfigByKey('app.delete.test', testUser.tenantId);
|
|
|
+
|
|
|
+ // 验证缓存已写入
|
|
|
+ let cachedValue = await redisUtil.getSystemConfig(testUser.tenantId, 'app.delete.test');
|
|
|
+ expect(cachedValue).toBe('to-be-deleted');
|
|
|
+
|
|
|
+ // 删除配置
|
|
|
+ await systemConfigService.deleteConfig('app.delete.test', testUser.tenantId);
|
|
|
+
|
|
|
+ // 验证缓存已清除
|
|
|
+ cachedValue = await redisUtil.getSystemConfig(testUser.tenantId, 'app.delete.test');
|
|
|
+ expect(cachedValue).toBeNull();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('缓存穿透保护测试', () => {
|
|
|
+ it('应该防止缓存穿透攻击', async () => {
|
|
|
+ const nonExistentKey = 'app.nonexistent.config';
|
|
|
+
|
|
|
+ // 第一次查询不存在的配置
|
|
|
+ const firstResult = await systemConfigService.getConfigByKey(nonExistentKey, testUser.tenantId);
|
|
|
+ expect(firstResult).toBeNull();
|
|
|
+
|
|
|
+ // 验证空值缓存已设置
|
|
|
+ const cachedValue = await redisUtil.getSystemConfig(testUser.tenantId, nonExistentKey);
|
|
|
+ expect(redisUtil.isNullValue(cachedValue)).toBe(true);
|
|
|
+
|
|
|
+ // 第二次查询应该从空值缓存返回null
|
|
|
+ const secondResult = await systemConfigService.getConfigByKey(nonExistentKey, testUser.tenantId);
|
|
|
+ expect(secondResult).toBeNull();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该在批量查询中防止缓存穿透', async () => {
|
|
|
+ const existentKey = 'app.existent.config';
|
|
|
+ const nonExistentKey = 'app.nonexistent.config';
|
|
|
+
|
|
|
+ // 创建一个存在的配置
|
|
|
+ await systemConfigService.create({
|
|
|
+ configKey: existentKey,
|
|
|
+ configValue: 'existent-value',
|
|
|
+ tenantId: testUser.tenantId
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ // 批量查询包含存在和不存在的配置
|
|
|
+ const result = await systemConfigService.getConfigsByKeys(
|
|
|
+ [existentKey, nonExistentKey],
|
|
|
+ testUser.tenantId
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result[existentKey]).toBe('existent-value');
|
|
|
+ expect(result[nonExistentKey]).toBeUndefined();
|
|
|
+
|
|
|
+ // 验证空值缓存已设置
|
|
|
+ const cachedValues = await redisUtil.getSystemConfigs(testUser.tenantId, [existentKey, nonExistentKey]);
|
|
|
+ expect(cachedValues[existentKey]).toBe('existent-value');
|
|
|
+ expect(redisUtil.isNullValue(cachedValues[nonExistentKey])).toBe(true);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('多租户缓存隔离测试', () => {
|
|
|
+ let tenant1User: any;
|
|
|
+ let tenant2User: any;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ // 创建租户1的用户
|
|
|
+ tenant1User = await TestDataFactory.createTestUser(dataSource, {
|
|
|
+ username: 'tenant1_user_cache',
|
|
|
+ password: 'TestPassword123!',
|
|
|
+ email: 'tenant1_cache@example.com',
|
|
|
+ tenantId: 1
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建租户2的用户
|
|
|
+ tenant2User = await TestDataFactory.createTestUser(dataSource, {
|
|
|
+ username: 'tenant2_user_cache',
|
|
|
+ password: 'TestPassword123!',
|
|
|
+ email: 'tenant2_cache@example.com',
|
|
|
+ tenantId: 2
|
|
|
+ });
|
|
|
+
|
|
|
+ // 清除测试前的缓存
|
|
|
+ await redisUtil.clearTenantSystemConfigs(1);
|
|
|
+ await redisUtil.clearTenantSystemConfigs(2);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该按租户隔离缓存', async () => {
|
|
|
+ const sharedConfigKey = 'app.shared.config';
|
|
|
+
|
|
|
+ // 为租户1创建配置
|
|
|
+ await systemConfigService.create({
|
|
|
+ configKey: sharedConfigKey,
|
|
|
+ configValue: 'tenant1-value',
|
|
|
+ tenantId: 1
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ // 为租户2创建配置
|
|
|
+ await systemConfigService.create({
|
|
|
+ configKey: sharedConfigKey,
|
|
|
+ configValue: 'tenant2-value',
|
|
|
+ tenantId: 2
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ // 查询租户1的配置
|
|
|
+ const tenant1Result = await systemConfigService.getConfigByKey(sharedConfigKey, 1);
|
|
|
+ expect(tenant1Result).toBe('tenant1-value');
|
|
|
+
|
|
|
+ // 查询租户2的配置
|
|
|
+ const tenant2Result = await systemConfigService.getConfigByKey(sharedConfigKey, 2);
|
|
|
+ expect(tenant2Result).toBe('tenant2-value');
|
|
|
+
|
|
|
+ // 验证缓存隔离
|
|
|
+ const tenant1Cached = await redisUtil.getSystemConfig(1, sharedConfigKey);
|
|
|
+ const tenant2Cached = await redisUtil.getSystemConfig(2, sharedConfigKey);
|
|
|
+ expect(tenant1Cached).toBe('tenant1-value');
|
|
|
+ expect(tenant2Cached).toBe('tenant2-value');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('缓存预热测试', () => {
|
|
|
+ it('应该成功预热缓存', async () => {
|
|
|
+ // 创建一些常用配置
|
|
|
+ await systemConfigService.create({
|
|
|
+ configKey: 'app.login.enabled',
|
|
|
+ configValue: 'true',
|
|
|
+ tenantId: testUser.tenantId
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ await systemConfigService.create({
|
|
|
+ configKey: 'app.payment.enabled',
|
|
|
+ configValue: 'false',
|
|
|
+ tenantId: testUser.tenantId
|
|
|
+ } as SystemConfigMt);
|
|
|
+
|
|
|
+ // 预热缓存
|
|
|
+ await systemConfigService.warmUpCache(testUser.tenantId);
|
|
|
+
|
|
|
+ // 验证缓存已预热
|
|
|
+ const cachedValues = await redisUtil.getSystemConfigs(testUser.tenantId, [
|
|
|
+ 'app.login.enabled',
|
|
|
+ 'app.payment.enabled',
|
|
|
+ 'app.notification.enabled'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ expect(cachedValues['app.login.enabled']).toBe('true');
|
|
|
+ expect(cachedValues['app.payment.enabled']).toBe('false');
|
|
|
+ expect(redisUtil.isNullValue(cachedValues['app.notification.enabled'])).toBe(true);
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|