黑盒测试用例

This commit is contained in:
wh
2026-04-14 20:00:37 +08:00
parent 999856e110
commit b0e2b3c81a
5 changed files with 1724 additions and 594 deletions

View File

@@ -0,0 +1,413 @@
package com.label.blackbox;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ByteArrayResource;
import javax.sql.DataSource;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
abstract class AbstractBlackBoxTest {
private static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(10);
private static final Properties APPLICATION = loadApplicationProperties();
protected final TestRestTemplate restTemplate = new TestRestTemplate();
protected final ObjectMapper objectMapper = new ObjectMapper();
protected JdbcTemplate jdbcTemplate;
protected String baseUrl;
protected String callbackSecret;
protected String runId;
protected Long companyId;
protected String companyCode;
protected String companyName;
protected TestUser adminUser;
protected TestUser reviewerUser;
protected TestUser annotatorUser;
protected TestUser annotator2User;
protected TestUser uploaderUser;
protected String adminToken;
protected String reviewerToken;
protected String annotatorToken;
protected String annotator2Token;
protected String uploaderToken;
protected boolean roleAwareAuthEnabled;
@BeforeEach
void setUpBlackBox(TestInfo testInfo) {
this.jdbcTemplate = new JdbcTemplate(createDataSource());
this.baseUrl = resolveBaseUrl();
this.callbackSecret = resolved("video.callback-secret", "");
this.runId = buildRunId(testInfo);
assertBackendReachable();
createIsolatedCompanyAndUsers();
issueTokens();
detectRuntimeAuthMode();
}
@AfterEach
void cleanUpBlackBox() {
if (companyId == null) {
return;
}
jdbcTemplate.update("DELETE FROM annotation_task_history WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM sys_operation_log WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM video_process_job WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM training_dataset WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM annotation_result WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM export_batch WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM annotation_task WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM source_data WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM sys_config WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM sys_user WHERE company_id = ?", companyId);
jdbcTemplate.update("DELETE FROM sys_company WHERE id = ?", companyId);
}
protected void requireRoleAwareAuth() {
org.junit.jupiter.api.Assumptions.assumeTrue(
roleAwareAuthEnabled,
"当前运行中的 backend 未启用真实多角色认证,跳过依赖角色/租户隔离的黑盒用例");
}
protected ResponseEntity<String> getRaw(String path) {
return restTemplate.getForEntity(url(path), String.class);
}
protected ResponseEntity<Map> get(String path, String token) {
return exchange(path, HttpMethod.GET, null, token, MediaType.APPLICATION_JSON);
}
protected ResponseEntity<Map> delete(String path, String token) {
return exchange(path, HttpMethod.DELETE, null, token, MediaType.APPLICATION_JSON);
}
protected ResponseEntity<Map> postJson(String path, Object body, String token) {
return exchange(path, HttpMethod.POST, body, token, MediaType.APPLICATION_JSON);
}
protected ResponseEntity<Map> putJson(String path, Object body, String token) {
return exchange(path, HttpMethod.PUT, body, token, MediaType.APPLICATION_JSON);
}
protected ResponseEntity<Map> upload(String path, String token, String filename, String dataType, byte[] bytes) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
if (token != null && !token.isBlank()) {
headers.setBearerAuth(token);
}
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("dataType", dataType);
body.add("file", new ByteArrayResource(bytes) {
@Override
public String getFilename() {
return filename;
}
});
return restTemplate.exchange(url(path), HttpMethod.POST, new HttpEntity<>(body, headers), Map.class);
}
protected ResponseEntity<Map> postVideoCallback(Map<String, Object> body) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
if (callbackSecret != null && !callbackSecret.isBlank()) {
headers.set("X-Callback-Secret", callbackSecret);
}
return restTemplate.exchange(url("/api/video/callback"), HttpMethod.POST, new HttpEntity<>(body, headers), Map.class);
}
protected String login(String targetCompanyCode, String username, String password) {
Map<String, Object> body = Map.of(
"companyCode", targetCompanyCode,
"username", username,
"password", password
);
ResponseEntity<Map> response = postJson("/api/auth/login", body, null);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) Objects.requireNonNull(response.getBody()).get("data");
return String.valueOf(data.get("token"));
}
protected Long uploadTextSource(String token) {
ResponseEntity<Map> response = upload(
"/api/source/upload",
token,
"bb-" + runId + ".txt",
"TEXT",
("hello-blackbox-" + runId).getBytes(StandardCharsets.UTF_8));
assertSuccess(response, HttpStatus.CREATED);
return dataId(response);
}
protected Long uploadVideoSource(String token) {
ResponseEntity<Map> response = upload(
"/api/source/upload",
token,
"bb-" + runId + ".mp4",
"VIDEO",
("fake-video-" + runId).getBytes(StandardCharsets.UTF_8));
assertSuccess(response, HttpStatus.CREATED);
return dataId(response);
}
protected Long createTask(Long sourceId, String taskType) {
ResponseEntity<Map> response = postJson("/api/tasks", Map.of(
"sourceId", sourceId,
"taskType", taskType
), adminToken);
assertSuccess(response, HttpStatus.OK);
return dataId(response);
}
protected Long latestTaskId(Long sourceId, String taskType) {
return jdbcTemplate.queryForObject(
"SELECT id FROM annotation_task WHERE company_id = ? AND source_id = ? AND task_type = ? " +
"ORDER BY id DESC LIMIT 1",
Long.class,
companyId, sourceId, taskType);
}
protected Long latestApprovedDatasetId(Long sourceId) {
return jdbcTemplate.queryForObject(
"SELECT id FROM training_dataset WHERE company_id = ? AND source_id = ? AND status = 'APPROVED' " +
"ORDER BY id DESC LIMIT 1",
Long.class,
companyId, sourceId);
}
protected Long insertFailedVideoJob(Long sourceId) {
return jdbcTemplate.queryForObject(
"INSERT INTO video_process_job (company_id, source_id, job_type, status, params, retry_count, max_retries) " +
"VALUES (?, ?, 'FRAME_EXTRACT', 'FAILED', '{}'::jsonb, 3, 3) RETURNING id",
Long.class,
companyId, sourceId);
}
protected Long insertPendingVideoJob(Long sourceId) {
return jdbcTemplate.queryForObject(
"INSERT INTO video_process_job (company_id, source_id, job_type, status, params, retry_count, max_retries) " +
"VALUES (?, ?, 'FRAME_EXTRACT', 'PENDING', '{}'::jsonb, 0, 3) RETURNING id",
Long.class,
companyId, sourceId);
}
protected void assertSuccess(ResponseEntity<Map> response, HttpStatus expectedStatus) {
assertThat(response.getStatusCode()).isEqualTo(expectedStatus);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().get("code")).isEqualTo("SUCCESS");
}
protected Long dataId(ResponseEntity<Map> response) {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) Objects.requireNonNull(response.getBody()).get("data");
return ((Number) data.get("id")).longValue();
}
protected boolean responseContainsId(ResponseEntity<Map> response, Long id) {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) Objects.requireNonNull(response.getBody()).get("data");
@SuppressWarnings("unchecked")
List<Map<String, Object>> items = (List<Map<String, Object>>) data.get("items");
return items.stream().anyMatch(item -> id.equals(((Number) item.get("id")).longValue()));
}
protected Long claimedByOfTask(Long taskId) {
return jdbcTemplate.queryForObject(
"SELECT claimed_by FROM annotation_task WHERE id = ?",
Long.class,
taskId);
}
protected String url(String path) {
return baseUrl + path;
}
private String resolveBaseUrl() {
String override = System.getProperty("blackbox.base-url");
if (override == null || override.isBlank()) {
override = System.getenv("BLACKBOX_BASE_URL");
}
if (override != null && !override.isBlank()) {
return override.endsWith("/") ? override.substring(0, override.length() - 1) : override;
}
return "http://127.0.0.1:" + resolved("server.port", "8080");
}
private ResponseEntity<Map> exchange(String path,
HttpMethod method,
Object body,
String token,
MediaType contentType) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(contentType);
if (token != null && !token.isBlank()) {
headers.setBearerAuth(token);
}
return restTemplate.exchange(url(path), method, new HttpEntity<>(body, headers), Map.class);
}
private void assertBackendReachable() {
try {
ResponseEntity<String> response = getRaw("/v3/api-docs");
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
} catch (Exception ex) {
fail("无法连接运行中的 backend请确认服务已按 application.yml 配置启动: " + ex.getMessage());
}
}
private void createIsolatedCompanyAndUsers() {
this.companyCode = ("BB" + runId).toUpperCase(Locale.ROOT);
this.companyName = "黑盒测试-" + runId;
this.companyId = jdbcTemplate.queryForObject(
"INSERT INTO sys_company (company_name, company_code, status) VALUES (?, ?, 'ACTIVE') RETURNING id",
Long.class,
companyName, companyCode);
this.adminUser = insertUser("admin", "ADMIN");
this.reviewerUser = insertUser("reviewer", "REVIEWER");
this.annotatorUser = insertUser("annotator", "ANNOTATOR");
this.annotator2User = insertUser("annotator2", "ANNOTATOR");
this.uploaderUser = insertUser("uploader", "UPLOADER");
}
private TestUser insertUser(String namePrefix, String role) {
String username = (namePrefix + "_" + runId).toLowerCase(Locale.ROOT);
String password = "Bb@" + runId;
Long userId = jdbcTemplate.queryForObject(
"INSERT INTO sys_user (company_id, username, password_hash, real_name, role, status) " +
"VALUES (?, ?, ?, ?, ?, 'ACTIVE') RETURNING id",
Long.class,
companyId,
username,
PASSWORD_ENCODER.encode(password),
"黑盒-" + namePrefix,
role);
return new TestUser(userId, username, password, role);
}
private void issueTokens() {
this.adminToken = login(companyCode, adminUser.username(), adminUser.password());
this.reviewerToken = login(companyCode, reviewerUser.username(), reviewerUser.password());
this.annotatorToken = login(companyCode, annotatorUser.username(), annotatorUser.password());
this.annotator2Token = login(companyCode, annotator2User.username(), annotator2User.password());
this.uploaderToken = login(companyCode, uploaderUser.username(), uploaderUser.password());
}
private void detectRuntimeAuthMode() {
try {
ResponseEntity<Map> adminMe = get("/api/auth/me", adminToken);
ResponseEntity<Map> reviewerMe = get("/api/auth/me", reviewerToken);
if (!adminMe.getStatusCode().is2xxSuccessful() || !reviewerMe.getStatusCode().is2xxSuccessful()) {
this.roleAwareAuthEnabled = false;
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> adminData = (Map<String, Object>) Objects.requireNonNull(adminMe.getBody()).get("data");
@SuppressWarnings("unchecked")
Map<String, Object> reviewerData = (Map<String, Object>) Objects.requireNonNull(reviewerMe.getBody()).get("data");
this.roleAwareAuthEnabled =
adminUser.username().equals(adminData.get("username"))
&& reviewerUser.username().equals(reviewerData.get("username"))
&& "ADMIN".equals(adminData.get("role"))
&& "REVIEWER".equals(reviewerData.get("role"))
&& companyId.equals(((Number) adminData.get("companyId")).longValue())
&& companyId.equals(((Number) reviewerData.get("companyId")).longValue());
} catch (Exception ex) {
this.roleAwareAuthEnabled = false;
}
}
private DataSource createDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(resolved("spring.datasource.driver-class-name", "org.postgresql.Driver"));
dataSource.setUrl(resolved("spring.datasource.url", ""));
dataSource.setUsername(resolved("spring.datasource.username", ""));
dataSource.setPassword(resolved("spring.datasource.password", ""));
return dataSource;
}
private String resolved(String key, String fallback) {
String raw = APPLICATION.getProperty(key);
if (raw == null || raw.isBlank()) {
return fallback;
}
return resolvePlaceholder(raw, fallback);
}
private static Properties loadApplicationProperties() {
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ClassPathResource("application.yml"));
Properties properties = yaml.getObject();
if (properties == null) {
throw new IllegalStateException("无法加载 application.yml");
}
return properties;
}
private static String resolvePlaceholder(String raw, String fallback) {
if (!raw.startsWith("${") || !raw.endsWith("}")) {
return raw;
}
String inner = raw.substring(2, raw.length() - 1);
int splitIndex = inner.indexOf(':');
if (splitIndex < 0) {
String envValue = System.getenv(inner);
return envValue != null ? envValue : fallback;
}
String envKey = inner.substring(0, splitIndex);
String defaultValue = inner.substring(splitIndex + 1);
String envValue = System.getenv(envKey);
return envValue != null ? envValue : defaultValue;
}
private static String buildRunId(TestInfo testInfo) {
String methodName = testInfo.getTestMethod().map(method -> method.getName()).orElse("case");
String normalized = methodName.replaceAll("[^a-zA-Z0-9]", "").toLowerCase(Locale.ROOT);
if (normalized.length() > 10) {
normalized = normalized.substring(0, 10);
}
return normalized + Long.toHexString(Instant.now().toEpochMilli()).substring(5) + UUID.randomUUID().toString().substring(0, 4);
}
protected record TestUser(Long id, String username, String password, String role) {
}
}

View File

@@ -0,0 +1,337 @@
package com.label.blackbox;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
class SwaggerLiveBlackBoxTest extends AbstractBlackBoxTest {
@Test
@DisplayName("公共接口与认证接口在真实运行环境下可访问")
void publicAndAuthEndpoints_shouldWork() {
ResponseEntity<String> openApi = getRaw("/v3/api-docs");
assertThat(openApi.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(openApi.getBody()).contains("/api/auth/login");
ResponseEntity<String> swaggerUi = getRaw("/swagger-ui.html");
assertThat(swaggerUi.getStatusCode().is2xxSuccessful() || swaggerUi.getStatusCode().is3xxRedirection()).isTrue();
ResponseEntity<Map> login = postJson("/api/auth/login", Map.of(
"companyCode", companyCode,
"username", adminUser.username(),
"password", adminUser.password()
), null);
assertSuccess(login, HttpStatus.OK);
if (!roleAwareAuthEnabled) {
return;
}
ResponseEntity<Map> me = get("/api/auth/me", adminToken);
assertSuccess(me, HttpStatus.OK);
@SuppressWarnings("unchecked")
Map<String, Object> meData = (Map<String, Object>) me.getBody().get("data");
assertThat(meData.get("username")).isEqualTo(adminUser.username());
assertThat(meData.get("role")).isEqualTo("ADMIN");
ResponseEntity<Map> logout = postJson("/api/auth/logout", null, adminToken);
assertSuccess(logout, HttpStatus.OK);
ResponseEntity<Map> meAfterLogout = get("/api/auth/me", adminToken);
assertThat(meAfterLogout.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@DisplayName("公司管理与用户管理接口在真实运行环境下可覆盖")
void companyAndUserEndpoints_shouldWork() {
requireRoleAwareAuth();
ResponseEntity<Map> companyList = get("/api/companies?page=1&pageSize=20", adminToken);
assertSuccess(companyList, HttpStatus.OK);
String extraCompanyCode = ("EXT" + runId).toUpperCase();
String extraCompanyName = "扩展公司-" + runId;
ResponseEntity<Map> createCompany = postJson("/api/companies", Map.of(
"companyName", extraCompanyName,
"companyCode", extraCompanyCode
), adminToken);
assertSuccess(createCompany, HttpStatus.CREATED);
Long extraCompanyId = dataId(createCompany);
ResponseEntity<Map> updateCompany = putJson("/api/companies/" + extraCompanyId, Map.of(
"companyName", extraCompanyName + "-改",
"companyCode", extraCompanyCode
), adminToken);
assertSuccess(updateCompany, HttpStatus.OK);
ResponseEntity<Map> companyStatus = putJson("/api/companies/" + extraCompanyId + "/status",
Map.of("status", "DISABLED"), adminToken);
assertSuccess(companyStatus, HttpStatus.OK);
ResponseEntity<Map> deleteCompany = delete("/api/companies/" + extraCompanyId, adminToken);
assertSuccess(deleteCompany, HttpStatus.OK);
ResponseEntity<Map> userList = get("/api/users?page=1&pageSize=20", adminToken);
assertSuccess(userList, HttpStatus.OK);
String username = "bb_user_" + UUID.randomUUID().toString().substring(0, 8);
String password = "BbUser@123";
ResponseEntity<Map> createUser = postJson("/api/users", Map.of(
"username", username,
"password", password,
"realName", "黑盒用户",
"role", "ANNOTATOR"
), adminToken);
assertSuccess(createUser, HttpStatus.OK);
Long userId = dataId(createUser);
ResponseEntity<Map> updateUser = putJson("/api/users/" + userId, Map.of(
"realName", "黑盒用户-改",
"password", "BbUser@456"
), adminToken);
assertSuccess(updateUser, HttpStatus.OK);
String userToken = login(companyCode, username, "BbUser@456");
ResponseEntity<Map> beforeRoleChange = get("/api/tasks/pending-review", userToken);
assertThat(beforeRoleChange.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
ResponseEntity<Map> updateRole = putJson("/api/users/" + userId + "/role",
Map.of("role", "REVIEWER"), adminToken);
assertSuccess(updateRole, HttpStatus.OK);
ResponseEntity<Map> afterRoleChange = get("/api/tasks/pending-review", userToken);
assertThat(afterRoleChange.getStatusCode()).isEqualTo(HttpStatus.OK);
ResponseEntity<Map> updateStatus = putJson("/api/users/" + userId + "/status",
Map.of("status", "DISABLED"), adminToken);
assertSuccess(updateStatus, HttpStatus.OK);
ResponseEntity<Map> meAfterDisable = get("/api/auth/me", userToken);
assertThat(meAfterDisable.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@DisplayName("资料与任务管理接口在真实运行环境下可覆盖")
void sourceAndTaskEndpoints_shouldWork() {
requireRoleAwareAuth();
Long disposableSourceId = uploadTextSource(uploaderToken);
ResponseEntity<Map> deleteDisposable = delete("/api/source/" + disposableSourceId, adminToken);
assertSuccess(deleteDisposable, HttpStatus.OK);
Long sourceId = uploadTextSource(uploaderToken);
ResponseEntity<Map> uploaderList = get("/api/source/list?page=1&pageSize=20", uploaderToken);
assertSuccess(uploaderList, HttpStatus.OK);
assertThat(responseContainsId(uploaderList, sourceId)).isTrue();
ResponseEntity<Map> adminList = get("/api/source/list?page=1&pageSize=20", adminToken);
assertSuccess(adminList, HttpStatus.OK);
assertThat(responseContainsId(adminList, sourceId)).isTrue();
ResponseEntity<Map> sourceDetail = get("/api/source/" + sourceId, adminToken);
assertSuccess(sourceDetail, HttpStatus.OK);
Long taskId = createTask(sourceId, "EXTRACTION");
ResponseEntity<Map> pool = get("/api/tasks/pool?page=1&pageSize=20", annotatorToken);
assertSuccess(pool, HttpStatus.OK);
assertThat(responseContainsId(pool, taskId)).isTrue();
ResponseEntity<Map> allTasks = get("/api/tasks?page=1&pageSize=20&taskType=EXTRACTION", adminToken);
assertSuccess(allTasks, HttpStatus.OK);
assertThat(responseContainsId(allTasks, taskId)).isTrue();
ResponseEntity<Map> taskDetail = get("/api/tasks/" + taskId, annotatorToken);
assertSuccess(taskDetail, HttpStatus.OK);
ResponseEntity<Map> claim = postJson("/api/tasks/" + taskId + "/claim", null, annotatorToken);
assertSuccess(claim, HttpStatus.OK);
ResponseEntity<Map> mine = get("/api/tasks/mine?page=1&pageSize=20", annotatorToken);
assertSuccess(mine, HttpStatus.OK);
assertThat(responseContainsId(mine, taskId)).isTrue();
ResponseEntity<Map> unclaim = postJson("/api/tasks/" + taskId + "/unclaim", null, annotatorToken);
assertSuccess(unclaim, HttpStatus.OK);
Long sourceId2 = uploadTextSource(uploaderToken);
Long taskId2 = createTask(sourceId2, "EXTRACTION");
ResponseEntity<Map> claimTask2 = postJson("/api/tasks/" + taskId2 + "/claim", null, annotatorToken);
assertSuccess(claimTask2, HttpStatus.OK);
ResponseEntity<Map> reassign = putJson("/api/tasks/" + taskId2 + "/reassign",
Map.of("userId", annotator2User.id()), adminToken);
assertSuccess(reassign, HttpStatus.OK);
assertThat(claimedByOfTask(taskId2)).isEqualTo(annotator2User.id());
}
@Test
@DisplayName("提取、问答、配置与导出接口在真实运行环境下可覆盖")
void extractionQaConfigAndExportEndpoints_shouldWork() {
requireRoleAwareAuth();
ResponseEntity<Map> listConfig = get("/api/config", adminToken);
assertSuccess(listConfig, HttpStatus.OK);
ResponseEntity<Map> updateConfig = putJson("/api/config/model_default",
Map.of("value", "glm-4-blackbox-" + runId, "description", "黑盒测试默认模型"),
adminToken);
assertSuccess(updateConfig, HttpStatus.OK);
Long sourceId = uploadTextSource(uploaderToken);
Long extractionTaskId = createTask(sourceId, "EXTRACTION");
assertSuccess(postJson("/api/tasks/" + extractionTaskId + "/claim", null, annotatorToken), HttpStatus.OK);
ResponseEntity<Map> extractionGet = get("/api/extraction/" + extractionTaskId, annotatorToken);
assertSuccess(extractionGet, HttpStatus.OK);
ResponseEntity<Map> extractionPut = putJson("/api/extraction/" + extractionTaskId,
"{\"items\":[{\"label\":\"entity\",\"text\":\"北京\"}]}",
annotatorToken);
assertSuccess(extractionPut, HttpStatus.OK);
ResponseEntity<Map> extractionSubmit = postJson("/api/extraction/" + extractionTaskId + "/submit", null, annotatorToken);
assertSuccess(extractionSubmit, HttpStatus.OK);
ResponseEntity<Map> pendingExtraction = get("/api/tasks/pending-review?page=1&pageSize=20&taskType=EXTRACTION", reviewerToken);
assertSuccess(pendingExtraction, HttpStatus.OK);
assertThat(responseContainsId(pendingExtraction, extractionTaskId)).isTrue();
ResponseEntity<Map> extractionReject = postJson("/api/extraction/" + extractionTaskId + "/reject",
Map.of("reason", "黑盒驳回一次"), reviewerToken);
assertSuccess(extractionReject, HttpStatus.OK);
ResponseEntity<Map> reclaim = postJson("/api/tasks/" + extractionTaskId + "/reclaim", null, annotatorToken);
assertSuccess(reclaim, HttpStatus.OK);
assertSuccess(putJson("/api/extraction/" + extractionTaskId,
"{\"items\":[{\"label\":\"entity\",\"text\":\"上海\"}]}",
annotatorToken), HttpStatus.OK);
assertSuccess(postJson("/api/extraction/" + extractionTaskId + "/submit", null, annotatorToken), HttpStatus.OK);
ResponseEntity<Map> extractionApprove = postJson("/api/extraction/" + extractionTaskId + "/approve", null, reviewerToken);
assertSuccess(extractionApprove, HttpStatus.OK);
Long qaTaskId = latestTaskId(sourceId, "QA_GENERATION");
assertThat(qaTaskId).isNotNull();
assertSuccess(postJson("/api/tasks/" + qaTaskId + "/claim", null, annotatorToken), HttpStatus.OK);
ResponseEntity<Map> qaGet = get("/api/qa/" + qaTaskId, annotatorToken);
assertSuccess(qaGet, HttpStatus.OK);
ResponseEntity<Map> qaPut = putJson("/api/qa/" + qaTaskId,
Map.of("items", List.of(Map.of("question", "北京在哪里", "answer", "中国"))),
annotatorToken);
assertSuccess(qaPut, HttpStatus.OK);
ResponseEntity<Map> qaSubmit = postJson("/api/qa/" + qaTaskId + "/submit", null, annotatorToken);
assertSuccess(qaSubmit, HttpStatus.OK);
ResponseEntity<Map> pendingQa = get("/api/tasks/pending-review?page=1&pageSize=20&taskType=QA_GENERATION", reviewerToken);
assertSuccess(pendingQa, HttpStatus.OK);
assertThat(responseContainsId(pendingQa, qaTaskId)).isTrue();
ResponseEntity<Map> qaApprove = postJson("/api/qa/" + qaTaskId + "/approve", null, reviewerToken);
assertSuccess(qaApprove, HttpStatus.OK);
ResponseEntity<Map> samples = get("/api/training/samples?page=1&pageSize=20&sampleType=TEXT", adminToken);
assertSuccess(samples, HttpStatus.OK);
Long datasetId = latestApprovedDatasetId(sourceId);
ResponseEntity<Map> createBatch = postJson("/api/export/batch",
Map.of("sampleIds", List.of(datasetId)), adminToken);
assertSuccess(createBatch, HttpStatus.CREATED);
Long batchId = dataId(createBatch);
ResponseEntity<Map> exportList = get("/api/export/list?page=1&pageSize=20", adminToken);
assertSuccess(exportList, HttpStatus.OK);
assertThat(responseContainsId(exportList, batchId)).isTrue();
ResponseEntity<Map> exportStatus = get("/api/export/" + batchId + "/status", adminToken);
assertSuccess(exportStatus, HttpStatus.OK);
// 第二条链路覆盖 QA reject
Long sourceId2 = uploadTextSource(uploaderToken);
Long extractionTaskId2 = createTask(sourceId2, "EXTRACTION");
assertSuccess(postJson("/api/tasks/" + extractionTaskId2 + "/claim", null, annotatorToken), HttpStatus.OK);
assertSuccess(postJson("/api/extraction/" + extractionTaskId2 + "/submit", null, annotatorToken), HttpStatus.OK);
assertSuccess(postJson("/api/extraction/" + extractionTaskId2 + "/approve", null, reviewerToken), HttpStatus.OK);
Long qaTaskId2 = latestTaskId(sourceId2, "QA_GENERATION");
assertSuccess(postJson("/api/tasks/" + qaTaskId2 + "/claim", null, annotatorToken), HttpStatus.OK);
assertSuccess(postJson("/api/qa/" + qaTaskId2 + "/submit", null, annotatorToken), HttpStatus.OK);
ResponseEntity<Map> qaReject = postJson("/api/qa/" + qaTaskId2 + "/reject",
Map.of("reason", "黑盒问答驳回"), reviewerToken);
assertSuccess(qaReject, HttpStatus.OK);
}
@Test
@DisplayName("视频处理与微调接口在显式开启重链路模式时可覆盖")
void videoAndFinetuneEndpoints_shouldWorkWhenHeavyModeEnabled() {
requireRoleAwareAuth();
assumeTrue(Boolean.getBoolean("blackbox.heavy.enabled"),
"未开启 -Dblackbox.heavy.enabled=true跳过视频处理与微调重链路黑盒用例");
Long sourceId = uploadTextSource(uploaderToken);
Long extractionTaskId = createTask(sourceId, "EXTRACTION");
assertSuccess(postJson("/api/tasks/" + extractionTaskId + "/claim", null, annotatorToken), HttpStatus.OK);
assertSuccess(postJson("/api/extraction/" + extractionTaskId + "/submit", null, annotatorToken), HttpStatus.OK);
assertSuccess(postJson("/api/extraction/" + extractionTaskId + "/approve", null, reviewerToken), HttpStatus.OK);
Long qaTaskId = latestTaskId(sourceId, "QA_GENERATION");
assertSuccess(postJson("/api/tasks/" + qaTaskId + "/claim", null, annotatorToken), HttpStatus.OK);
assertSuccess(postJson("/api/qa/" + qaTaskId + "/submit", null, annotatorToken), HttpStatus.OK);
assertSuccess(postJson("/api/qa/" + qaTaskId + "/approve", null, reviewerToken), HttpStatus.OK);
Long datasetId = latestApprovedDatasetId(sourceId);
ResponseEntity<Map> createBatch = postJson("/api/export/batch",
Map.of("sampleIds", List.of(datasetId)), adminToken);
assertSuccess(createBatch, HttpStatus.CREATED);
Long batchId = dataId(createBatch);
ResponseEntity<Map> finetune = postJson("/api/export/" + batchId + "/finetune", null, adminToken);
assertSuccess(finetune, HttpStatus.OK);
Long videoSourceId = uploadVideoSource(uploaderToken);
ResponseEntity<Map> createVideoJob = postJson("/api/video/process",
Map.of("sourceId", videoSourceId, "jobType", "FRAME_EXTRACT", "params", "{\"frameInterval\":30}"),
adminToken);
assertSuccess(createVideoJob, HttpStatus.OK);
Long jobId = dataId(createVideoJob);
ResponseEntity<Map> getVideoJob = get("/api/video/jobs/" + jobId, adminToken);
assertSuccess(getVideoJob, HttpStatus.OK);
Long failedJobId = insertFailedVideoJob(videoSourceId);
ResponseEntity<Map> resetJob = postJson("/api/video/jobs/" + failedJobId + "/reset", null, adminToken);
assertSuccess(resetJob, HttpStatus.OK);
Long callbackJobId = insertPendingVideoJob(videoSourceId);
ResponseEntity<Map> callbackSuccess1 = postVideoCallback(Map.of(
"jobId", callbackJobId,
"status", "SUCCESS",
"outputPath", "processed/" + runId + "/frames.zip"
));
assertSuccess(callbackSuccess1, HttpStatus.OK);
ResponseEntity<Map> callbackSuccess2 = postVideoCallback(Map.of(
"jobId", callbackJobId,
"status", "SUCCESS",
"outputPath", "processed/" + runId + "/frames.zip"
));
assertSuccess(callbackSuccess2, HttpStatus.OK);
}
}