Phase 4 完成:US2 原始资料上传(SourceData / SourceService / SourceController)

新增:
- SourceData 实体 + SourceDataMapper(含 updateStatus 方法)
- SourceResponse DTO(上传/列表/详情复用)
- SourceService(upload/list/findById/delete,upload 先 INSERT 获取 ID
  再构造 RustFS 路径,delete 仅允许 PENDING 状态)
- SourceController(POST /api/source/upload 返回 201,GET /list,
  GET /{id},DELETE /{id};@RequiresRoles 声明权限)
- SourceIntegrationTest(权限校验、空列表、删除不存在资料、
  已进入流水线资料删除返回 409)
- application.yml 添加 token.ttl-seconds 配置项
This commit is contained in:
wh
2026-04-09 15:21:32 +08:00
parent a28fecd16a
commit 7f12fc520a
6 changed files with 577 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
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.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 原始资料管理集成测试US2
*
* 测试场景:
* 1. UPLOADER 上传文本 → 列表仅返回自己的资料
* 2. ADMIN 查看列表 → 返回全公司资料
* 3. 上传视频 → status = PENDING视频预处理由 Phase 9 处理)
* 4. 已进入流水线的资料删除 → 409 SOURCE_IN_PIPELINE
*
* 注意:本测试不连接真实 RustFS上传操作会失败并返回 500/503。
* 测试仅验证可访问的业务逻辑(权限、状态机)。
* 如需覆盖文件上传,需在测试环境配置 Mock RustFsClient 或启动 MinIO 容器。
*/
public class SourceIntegrationTest extends AbstractIntegrationTest {
private static final String UPLOADER_TOKEN = "test-uploader-token-source";
private static final String UPLOADER2_TOKEN = "test-uploader2-token-source";
private static final String ADMIN_TOKEN = "test-admin-token-source";
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private RedisService redisService;
@BeforeEach
void setupTokens() {
// uploader01 token (userId=4 from init.sql seed)
redisService.hSetAll(RedisKeyManager.tokenKey(UPLOADER_TOKEN),
Map.of("userId", "4", "role", "UPLOADER", "companyId", "1", "username", "uploader01"),
3600L);
// admin token (userId=1 from init.sql seed)
redisService.hSetAll(RedisKeyManager.tokenKey(ADMIN_TOKEN),
Map.of("userId", "1", "role", "ADMIN", "companyId", "1", "username", "admin"),
3600L);
}
@AfterEach
void cleanupTokens() {
redisService.delete(RedisKeyManager.tokenKey(UPLOADER_TOKEN));
redisService.delete(RedisKeyManager.tokenKey(UPLOADER2_TOKEN));
redisService.delete(RedisKeyManager.tokenKey(ADMIN_TOKEN));
}
// ------------------------------------------------------------------ 权限测试 --
@Test
@DisplayName("无 Token 访问上传接口 → 401")
void upload_withoutToken_returns401() {
ResponseEntity<String> response = restTemplate.postForEntity(
baseUrl("/api/source/upload"), null, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@DisplayName("UPLOADER 访问列表接口(无数据)→ 200items 为空")
void list_uploaderWithNoData_returnsEmptyList() {
ResponseEntity<Map> response = restTemplate.exchange(
baseUrl("/api/source/list"),
HttpMethod.GET,
bearerRequest(UPLOADER_TOKEN),
Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
assertThat(data.get("items")).isInstanceOf(List.class);
assertThat(((List<?>) data.get("items"))).isEmpty();
assertThat(((Number) data.get("total")).longValue()).isEqualTo(0L);
}
@Test
@DisplayName("ADMIN 访问列表接口(无数据)→ 200items 为空")
void list_adminWithNoData_returnsEmptyList() {
ResponseEntity<Map> response = restTemplate.exchange(
baseUrl("/api/source/list"),
HttpMethod.GET,
bearerRequest(ADMIN_TOKEN),
Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
assertThat(((List<?>) data.get("items"))).isEmpty();
}
@Test
@DisplayName("删除不存在的资料 → 404")
void delete_nonExistentSource_returns404() {
ResponseEntity<Map> response = restTemplate.exchange(
baseUrl("/api/source/9999"),
HttpMethod.DELETE,
bearerRequest(ADMIN_TOKEN),
Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
@DisplayName("非 ADMIN 删除资料 → 403 Forbidden")
void delete_byUploader_returns403() {
ResponseEntity<Map> response = restTemplate.exchange(
baseUrl("/api/source/9999"),
HttpMethod.DELETE,
bearerRequest(UPLOADER_TOKEN),
Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
@DisplayName("ADMIN 删除已进入流水线的资料 → 409 SOURCE_IN_PIPELINE")
void delete_sourceInPipeline_returns409() {
// 直接向 DB 插入一条 EXTRACTING 状态的资料(模拟已进入流水线)
jdbcTemplate.execute(
"INSERT INTO source_data (company_id, uploader_id, data_type, file_path, " +
"file_name, file_size, bucket_name, status) " +
"VALUES (1, 1, 'TEXT', 'test/path/file.txt', 'file.txt', 100, 'test-bucket', 'EXTRACTING')");
Long sourceId = jdbcTemplate.queryForObject(
"SELECT id FROM source_data WHERE status='EXTRACTING' LIMIT 1", Long.class);
assertThat(sourceId).isNotNull();
ResponseEntity<Map> response = restTemplate.exchange(
baseUrl("/api/source/" + sourceId),
HttpMethod.DELETE,
bearerRequest(ADMIN_TOKEN),
Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
@SuppressWarnings("unchecked")
Map<String, Object> body = response.getBody();
assertThat(body.get("code")).isEqualTo("SOURCE_IN_PIPELINE");
}
// ------------------------------------------------------------------ 工具方法 --
private HttpEntity<Void> bearerRequest(String token) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
return new HttpEntity<>(headers);
}
}