2026-04-09 16:18:39 +08:00
|
|
|
|
package com.label.integration;
|
|
|
|
|
|
|
|
|
|
|
|
import com.label.AbstractIntegrationTest;
|
2026-04-14 14:59:46 +08:00
|
|
|
|
import com.label.service.RedisService;
|
|
|
|
|
|
import com.label.util.RedisKeyManager;
|
|
|
|
|
|
|
2026-04-09 16:18:39 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|