package com.label.integration; import com.label.AbstractIntegrationTest; import com.label.dto.LoginRequest; 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.Map; import static org.assertj.core.api.Assertions.assertThat; /** * 鎻愬彇闃舵瀹℃壒闆嗘垚娴嬭瘯锛圲S4锛夈€? * * 娴嬭瘯鍦烘櫙锛? * 1. 瀹℃壒閫氳繃 鈫?QA_GENERATION 浠诲姟鑷姩鍒涘缓锛宻ource_data 鐘舵€佹洿鏂颁负 QA_REVIEW * 2. 瀹℃壒浜轰笌鎻愪氦浜虹浉鍚岋紙鑷锛夆啋 403 SELF_REVIEW_FORBIDDEN * 3. 椹冲洖鍚庢爣娉ㄥ憳鍙噸棰嗕换鍔″苟鍐嶆鎻愪氦 */ public class ExtractionApprovalIntegrationTest extends AbstractIntegrationTest { @Autowired private TestRestTemplate restTemplate; private Long sourceId; private Long taskId; private Long annotatorUserId; private Long reviewerUserId; @BeforeEach void setup() { // 鑾峰彇绉嶅瓙鐢ㄦ埛 ID锛坕nit.sql 涓凡鎻掑叆锛? annotatorUserId = jdbcTemplate.queryForObject( "SELECT id FROM sys_user WHERE username = 'annotator01'", Long.class); reviewerUserId = jdbcTemplate.queryForObject( "SELECT id FROM sys_user WHERE username = 'reviewer01'", Long.class); Long companyId = jdbcTemplate.queryForObject( "SELECT id FROM sys_company WHERE company_code = 'DEMO'", Long.class); // 鎻掑叆娴嬭瘯 source_data jdbcTemplate.execute( "INSERT INTO source_data (company_id, uploader_id, data_type, file_path, " + "file_name, file_size, bucket_name, status) " + "VALUES (" + companyId + ", " + annotatorUserId + ", 'TEXT', " + "'test/approval-test/file.txt', 'file.txt', 100, 'test-bucket', 'PENDING')"); sourceId = jdbcTemplate.queryForObject( "SELECT id FROM source_data ORDER BY id DESC LIMIT 1", Long.class); // 鎻掑叆 UNCLAIMED EXTRACTION 浠诲姟 jdbcTemplate.execute( "INSERT INTO annotation_task (company_id, source_id, task_type, status) " + "VALUES (" + companyId + ", " + sourceId + ", 'EXTRACTION', 'UNCLAIMED')"); taskId = jdbcTemplate.queryForObject( "SELECT id FROM annotation_task ORDER BY id DESC LIMIT 1", Long.class); } // ------------------------------------------------------------------ 娴嬭瘯 1: 瀹℃壒閫氳繃 鈫?QA 浠诲姟鑷姩鍒涘缓 -- @Test @DisplayName("瀹℃壒閫氳繃鍚庯紝QA_GENERATION 浠诲姟鑷姩鍒涘缓锛宻ource_data 鐘舵€佸彉涓?QA_REVIEW") void approveTask_thenQaTaskAndSourceStatusUpdated() { String annotatorToken = loginAndGetToken("DEMO", "annotator01", "annot123"); String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123"); // 1. 鏍囨敞鍛橀鍙栦换鍔? ResponseEntity claimResp = restTemplate.exchange( baseUrl("/api/tasks/" + taskId + "/claim"), HttpMethod.POST, bearerRequest(annotatorToken), Map.class); assertThat(claimResp.getStatusCode()).isEqualTo(HttpStatus.OK); // 2. 鏍囨敞鍛樻彁浜ゆ爣娉? ResponseEntity submitResp = restTemplate.exchange( baseUrl("/api/extraction/" + taskId + "/submit"), HttpMethod.POST, bearerRequest(annotatorToken), Map.class); assertThat(submitResp.getStatusCode()).isEqualTo(HttpStatus.OK); // 3. 瀹℃牳鍛樺鎵归€氳繃 // 娉細ExtractionApprovedEventListener(@TransactionalEventListener AFTER_COMMIT) // 鍦ㄥ悓涓€绾跨▼涓悓姝ユ墽琛岋紝HTTP 鍝嶅簲杩斿洖鍓嶅凡瀹屾垚鍚庣画澶勭悊 ResponseEntity approveResp = restTemplate.exchange( baseUrl("/api/extraction/" + taskId + "/approve"), HttpMethod.POST, bearerRequest(reviewerToken), Map.class); assertThat(approveResp.getStatusCode()).isEqualTo(HttpStatus.OK); // 楠岃瘉锛氬師浠诲姟鐘舵€佸彉涓?APPROVED锛宨s_final=true Map taskRow = jdbcTemplate.queryForMap( "SELECT status, is_final FROM annotation_task WHERE id = ?", taskId); assertThat(taskRow.get("status")).isEqualTo("APPROVED"); assertThat(taskRow.get("is_final")).isEqualTo(Boolean.TRUE); // 楠岃瘉锛歈A_GENERATION 浠诲姟宸茶嚜鍔ㄥ垱寤猴紙UNCLAIMED 鐘舵€侊級 Integer qaTaskCount = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM annotation_task " + "WHERE source_id = ? AND task_type = 'QA_GENERATION' AND status = 'UNCLAIMED'", Integer.class, sourceId); assertThat(qaTaskCount).as("QA_GENERATION 浠诲姟搴斿凡鍒涘缓").isEqualTo(1); // 楠岃瘉锛歴ource_data 鐘舵€佸凡鏇存柊涓?QA_REVIEW String sourceStatus = jdbcTemplate.queryForObject( "SELECT status FROM source_data WHERE id = ?", String.class, sourceId); assertThat(sourceStatus).as("source_data 鐘舵€佸簲涓?QA_REVIEW").isEqualTo("QA_REVIEW"); // 楠岃瘉锛歵raining_dataset 宸蹭互 PENDING_REVIEW 鐘舵€佸垱寤? Integer datasetCount = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM training_dataset " + "WHERE source_id = ? AND status = 'PENDING_REVIEW'", Integer.class, sourceId); assertThat(datasetCount).as("training_dataset 搴斿凡鍒涘缓").isEqualTo(1); } // ------------------------------------------------------------------ 娴嬭瘯 2: 鑷杩斿洖 403 -- @Test @DisplayName("瀹℃壒浜轰笌浠诲姟棰嗗彇浜虹浉鍚岋紙鑷锛夆啋 403 SELF_REVIEW_FORBIDDEN") void approveOwnSubmission_returnsForbidden() { // 鐩存帴灏嗕换鍔$疆涓?SUBMITTED 骞惰 claimed_by = reviewer01锛堟ā鎷熻嚜瀹″満鏅級 jdbcTemplate.execute( "UPDATE annotation_task " + "SET status = 'SUBMITTED', claimed_by = " + reviewerUserId + ", claimed_at = NOW(), submitted_at = NOW() " + "WHERE id = " + taskId); String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123"); ResponseEntity resp = restTemplate.exchange( baseUrl("/api/extraction/" + taskId + "/approve"), HttpMethod.POST, bearerRequest(reviewerToken), Map.class); assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); // 楠岃瘉浠诲姟鐘舵€佹湭鍙? String status = jdbcTemplate.queryForObject( "SELECT status FROM annotation_task WHERE id = ?", String.class, taskId); assertThat(status).isEqualTo("SUBMITTED"); } // ------------------------------------------------------------------ 娴嬭瘯 3: 椹冲洖 鈫?閲嶉 鈫?鍐嶆彁浜?-- @Test @DisplayName("椹冲洖鍚庢爣娉ㄥ憳鍙噸棰嗕换鍔″苟鍐嶆鎻愪氦锛屼换鍔$姸鎬佹仮澶嶄负 SUBMITTED") void rejectThenReclaimAndResubmit_succeeds() { String annotatorToken = loginAndGetToken("DEMO", "annotator01", "annot123"); String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123"); // 1. 鏍囨敞鍛橀鍙栧苟鎻愪氦 restTemplate.exchange(baseUrl("/api/tasks/" + taskId + "/claim"), HttpMethod.POST, bearerRequest(annotatorToken), Map.class); restTemplate.exchange(baseUrl("/api/extraction/" + taskId + "/submit"), HttpMethod.POST, bearerRequest(annotatorToken), Map.class); // 2. 瀹℃牳鍛橀┏鍥烇紙椹冲洖鍘熷洜蹇呭~锛? HttpHeaders rejectHeaders = new HttpHeaders(); rejectHeaders.set("Authorization", "Bearer " + reviewerToken); rejectHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity> rejectReq = new HttpEntity<>( Map.of("reason", "瀹炰綋璇嗗埆鏈夎锛岃閲嶆柊鏍囨敞"), rejectHeaders); ResponseEntity rejectResp = restTemplate.exchange( baseUrl("/api/extraction/" + taskId + "/reject"), HttpMethod.POST, rejectReq, Map.class); assertThat(rejectResp.getStatusCode()).isEqualTo(HttpStatus.OK); // 楠岃瘉锛氫换鍔$姸鎬佸彉涓?REJECTED String statusAfterReject = jdbcTemplate.queryForObject( "SELECT status FROM annotation_task WHERE id = ?", String.class, taskId); assertThat(statusAfterReject).isEqualTo("REJECTED"); // 3. 鏍囨敞鍛橀噸棰嗕换鍔★紙REJECTED 鈫?IN_PROGRESS锛? ResponseEntity reclaimResp = restTemplate.exchange( baseUrl("/api/tasks/" + taskId + "/reclaim"), HttpMethod.POST, bearerRequest(annotatorToken), Map.class); assertThat(reclaimResp.getStatusCode()).isEqualTo(HttpStatus.OK); // 楠岃瘉锛氫换鍔$姸鎬佹仮澶嶄负 IN_PROGRESS String statusAfterReclaim = jdbcTemplate.queryForObject( "SELECT status FROM annotation_task WHERE id = ?", String.class, taskId); assertThat(statusAfterReclaim).isEqualTo("IN_PROGRESS"); // 4. 鏍囨敞鍛樺啀娆℃彁浜わ紙IN_PROGRESS 鈫?SUBMITTED锛? ResponseEntity resubmitResp = restTemplate.exchange( baseUrl("/api/extraction/" + taskId + "/submit"), HttpMethod.POST, bearerRequest(annotatorToken), Map.class); assertThat(resubmitResp.getStatusCode()).isEqualTo(HttpStatus.OK); // 楠岃瘉锛氫换鍔$姸鎬佸彉涓?SUBMITTED String finalStatus = jdbcTemplate.queryForObject( "SELECT status FROM annotation_task WHERE id = ?", String.class, taskId); assertThat(finalStatus).isEqualTo("SUBMITTED"); } // ------------------------------------------------------------------ 宸ュ叿鏂规硶 -- private String loginAndGetToken(String companyCode, String username, String password) { LoginRequest req = new LoginRequest(); req.setCompanyCode(companyCode); req.setUsername(username); req.setPassword(password); ResponseEntity response = restTemplate.postForEntity( baseUrl("/api/auth/login"), req, Map.class); if (!response.getStatusCode().is2xxSuccessful()) { return null; } @SuppressWarnings("unchecked") Map data = (Map) response.getBody().get("data"); return (String) data.get("token"); } private HttpEntity bearerRequest(String token) { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); return new HttpEntity<>(headers); } }