Files
label_backend/src/test/java/com/label/integration/SysConfigIntegrationTest.java
wh a14c3f5559 feat(phase9-10): US8 视频处理与系统配置模块 + 代码审查修复
Phase 9 (US8):
- VideoProcessJob 实体 + VideoProcessJobMapper
- SysConfig 实体 + SysConfigMapper(手动多租户查询)
- VideoProcessService:createJob/handleCallback(幂等)/reset
  - T074 修复:AI 触发通过 TransactionSynchronization.afterCommit() 延迟至事务提交后
- VideoController:4 个端点,/api/video/callback 无需认证
- SysConfigService:公司专属优先 > 全局默认回退,UPSERT 仅允许已知键
- SysConfigController:GET /api/config + PUT /api/config/{key}
- TokenFilter:/api/video/callback 绕过 Token 认证
- 集成测试:VideoCallbackIdempotencyTest、SysConfigIntegrationTest

Phase 10 (代码审查与修复):
- T070 MultiTenantIsolationTest:跨公司资料/配置隔离验证
- T071 SourceController.upload():ResponseEntity<Result<T>> → Result<T> + @ResponseStatus
- T074 FinetuneService.trigger():移除 @Transactional,AI 调用在事务外执行
2026-04-09 16:18:39 +08:00

184 lines
7.2 KiB
Java
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.

package com.label.integration;
import com.label.AbstractIntegrationTest;
import com.label.common.redis.RedisKeyManager;
import com.label.common.redis.RedisService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 系统配置集成测试US8
*
* 测试场景:
* 1. 公司专属配置覆盖全局默认
* 2. 未设置公司专属时,回退至全局默认
* 3. 未知配置键 → 400 UNKNOWN_CONFIG_KEY
*/
public class SysConfigIntegrationTest extends AbstractIntegrationTest {
private static final String ADMIN_TOKEN = "test-admin-token-config";
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private RedisService redisService;
private Long companyId;
private Long adminUserId;
@BeforeEach
void setupToken() {
companyId = jdbcTemplate.queryForObject(
"SELECT id FROM sys_company WHERE company_code = 'DEMO'", Long.class);
adminUserId = jdbcTemplate.queryForObject(
"SELECT id FROM sys_user WHERE username = 'admin'", Long.class);
// 伪造 Redis Token
redisService.hSetAll(RedisKeyManager.tokenKey(ADMIN_TOKEN),
Map.of("userId", adminUserId.toString(), "role", "ADMIN",
"companyId", companyId.toString(), "username", "admin"),
3600L);
}
@AfterEach
void cleanupTokens() {
redisService.delete(RedisKeyManager.tokenKey(ADMIN_TOKEN));
}
// ------------------------------------------------------------------ 测试 1: 公司配置覆盖全局 --
@Test
@DisplayName("公司专属配置优先于全局默认scope=COMPANY 覆盖 scope=GLOBAL")
void companyConfig_overridesGlobalDefault() {
// 设置公司专属配置(覆盖全局 model_default
updateConfig("model_default", "glm-4-plus", "公司专属模型");
// 查询配置列表
ResponseEntity<Map> listResp = restTemplate.exchange(
baseUrl("/api/config"),
HttpMethod.GET,
bearerRequest(ADMIN_TOKEN),
Map.class);
assertThat(listResp.getStatusCode()).isEqualTo(HttpStatus.OK);
@SuppressWarnings("unchecked")
List<Map<String, Object>> configs = (List<Map<String, Object>>) listResp.getBody().get("data");
assertThat(configs).isNotEmpty();
// 找到 model_default 配置
Map<String, Object> modelConfig = configs.stream()
.filter(c -> "model_default".equals(c.get("configKey")))
.findFirst()
.orElseThrow(() -> new AssertionError("model_default 配置不存在"));
// 应返回公司专属配置值scope=COMPANY
assertThat(modelConfig.get("configValue"))
.as("公司专属配置应覆盖全局默认")
.isEqualTo("glm-4-plus");
assertThat(modelConfig.get("scope"))
.as("scope 应标记为 COMPANY")
.isEqualTo("COMPANY");
}
// ------------------------------------------------------------------ 测试 2: 回退全局默认 --
@Test
@DisplayName("未设置公司专属配置时返回全局默认值scope=GLOBAL")
void globalConfig_usedWhenNoCompanyOverride() {
// 不设置公司专属,直接查询列表
ResponseEntity<Map> listResp = restTemplate.exchange(
baseUrl("/api/config"),
HttpMethod.GET,
bearerRequest(ADMIN_TOKEN),
Map.class);
assertThat(listResp.getStatusCode()).isEqualTo(HttpStatus.OK);
@SuppressWarnings("unchecked")
List<Map<String, Object>> configs = (List<Map<String, Object>>) listResp.getBody().get("data");
// 至少包含 AbstractIntegrationTest.cleanData() 中插入的全局配置
assertThat(configs).isNotEmpty();
// 所有配置都应有 scope 字段
configs.forEach(cfg ->
assertThat(cfg.containsKey("scope")).as("每条配置应含 scope 字段").isTrue());
// token_ttl_seconds 全局默认应为 7200
Map<String, Object> ttlConfig = configs.stream()
.filter(c -> "token_ttl_seconds".equals(c.get("configKey")))
.findFirst()
.orElse(null);
if (ttlConfig != null) {
assertThat(ttlConfig.get("configValue")).isEqualTo("7200");
assertThat(ttlConfig.get("scope")).isEqualTo("GLOBAL");
}
}
// ------------------------------------------------------------------ 测试 3: 未知配置键 --
@Test
@DisplayName("更新未知配置键 → 400 UNKNOWN_CONFIG_KEY")
void updateUnknownKey_returns400() {
ResponseEntity<Map> resp = updateConfig("unknown_key_xyz", "someValue", null);
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(resp.getBody().get("code")).isEqualTo("UNKNOWN_CONFIG_KEY");
}
// ------------------------------------------------------------------ 测试 4: UPSERT 同键两次 --
@Test
@DisplayName("同一配置键两次 PUT → 第二次更新而非重复插入")
void updateSameKey_twice_upserts() {
updateConfig("video_frame_interval", "60", "帧间隔 60s");
updateConfig("video_frame_interval", "120", "帧间隔 120s");
// 数据库中公司专属 video_frame_interval 应只有一条记录
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM sys_config WHERE company_id = ? AND config_key = 'video_frame_interval'",
Integer.class, companyId);
assertThat(count).as("UPSERT同键应只有一条公司专属记录").isEqualTo(1);
// 值应为最后一次 PUT 的值
String value = jdbcTemplate.queryForObject(
"SELECT config_value FROM sys_config WHERE company_id = ? AND config_key = 'video_frame_interval'",
String.class, companyId);
assertThat(value).isEqualTo("120");
}
// ------------------------------------------------------------------ 工具方法 --
private ResponseEntity<Map> updateConfig(String key, String value, String description) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + ADMIN_TOKEN);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, String> body = description != null
? Map.of("value", value, "description", description)
: Map.of("value", value);
return restTemplate.exchange(
baseUrl("/api/config/" + key),
HttpMethod.PUT,
new HttpEntity<>(body, headers),
Map.class);
}
private HttpEntity<Void> bearerRequest(String token) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
return new HttpEntity<>(headers);
}
}