afcd18c54f
Backend: - Add NotFoundException + BusinessException, return correct HTTP status (404/400) - Add @Index on reports.project_id and reports.upload_time - Add fileSize column to reports, populate on upload, return in DTO - Cascade delete: deleting project now removes all reports (DB + files + PDFs) - Delete report: also clean up pre-rendered PDF - File upload MIME validation (extension + Content-Type) - Remove duplicate @ExceptionHandler from ReportController - Switch from System.err to SLF4J logger - Handle MethodArgumentNotValid, MissingServletRequestPart, etc. Frontend: - Remove all Docker files (project uses 宝塔 panel deployment) - Upgrade axios 1.6.8 -> 1.7.7 (CVE-2024-39338) - Remove unused @vue-office/pptx + vue-demi (see CHANGELOG for rationale) - Fix vite proxy port 37821 -> 30081 - Remove mock data fallback in production - Add upload report UI (button + modal in ProjectDetail) - Add create project UI (button + modal in ProjectList) - Add filename search box in ProjectDetail - New useApi methods: createProject, uploadReport, deleteProject, deleteReport - FilePreview/ReportCard: show fileSize (was undefined before) Docs: - Add README.md (overview, quick start, structure) - Add CHANGELOG.md (full change log + pptx removal rationale) - Include EVALUATION_REPORT.md and blog-vibe-coding.md Tests: - All 73 backend tests pass - All 43 frontend tests pass - Updated test fixtures for new API contract
251 lines
10 KiB
Java
251 lines
10 KiB
Java
package com.reportdist;
|
|
|
|
import com.reportdist.dto.ProjectRequest;
|
|
import com.reportdist.dto.ReportResponse;
|
|
import org.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
import org.springframework.boot.test.web.client.TestRestTemplate;
|
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
|
import org.springframework.core.io.ByteArrayResource;
|
|
import org.springframework.http.*;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.springframework.util.LinkedMultiValueMap;
|
|
import org.springframework.util.MultiValueMap;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
/**
|
|
* Complete API flow integration test.
|
|
* Tests: Create Project → Upload Report → Query Report → Delete Report → Delete Project (cascade)
|
|
*/
|
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
class CompleteApiFlowIntegrationTest {
|
|
|
|
@LocalServerPort
|
|
private int port;
|
|
|
|
@Autowired
|
|
private TestRestTemplate restTemplate;
|
|
|
|
private String baseUrl;
|
|
|
|
@BeforeEach
|
|
void setUp() throws Exception {
|
|
baseUrl = "http://localhost:" + port;
|
|
// Ensure upload directory exists
|
|
Path uploadPath = Paths.get(System.getProperty("java.io.tmpdir"), "report-dist-test-uploads");
|
|
if (!Files.exists(uploadPath)) {
|
|
Files.createDirectories(uploadPath);
|
|
}
|
|
}
|
|
|
|
@AfterEach
|
|
void tearDown() {
|
|
// Cleanup is handled by H2 create-drop and file cleanup in each test
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Complete API flow: Create Project → Upload Report → Query Report → Delete Report → Delete Project")
|
|
void completeApiFlow_shouldSucceed() {
|
|
// Step 1: Create a project
|
|
ProjectRequest projectRequest = new ProjectRequest("Integration Test Project", "Testing complete flow");
|
|
ResponseEntity<Map> createResponse = restTemplate.postForEntity(
|
|
baseUrl + "/api/projects",
|
|
projectRequest,
|
|
Map.class
|
|
);
|
|
|
|
assertEquals(HttpStatus.CREATED, createResponse.getStatusCode());
|
|
assertNotNull(createResponse.getBody());
|
|
Long projectId = ((Number) createResponse.getBody().get("id")).longValue();
|
|
assertNotNull(projectId);
|
|
assertEquals("Integration Test Project", createResponse.getBody().get("name"));
|
|
System.out.println("[Step 1] Created project with ID: " + projectId);
|
|
|
|
// Step 2: Verify project exists
|
|
ResponseEntity<Map> getProjectResponse = restTemplate.getForEntity(
|
|
baseUrl + "/api/projects/" + projectId,
|
|
Map.class
|
|
);
|
|
assertEquals(HttpStatus.OK, getProjectResponse.getStatusCode());
|
|
assertEquals("Integration Test Project", getProjectResponse.getBody().get("name"));
|
|
System.out.println("[Step 2] Verified project exists");
|
|
|
|
// Step 3: Upload a report to the project
|
|
String htmlContent = "<html><body><h1>Integration Test Report</h1><p>Content here</p></body></html>";
|
|
MultiValueMap<String, Object> reportParts = new LinkedMultiValueMap<>();
|
|
reportParts.add("file", new ByteArrayResource(htmlContent.getBytes()) {
|
|
@Override
|
|
public String getFilename() {
|
|
return "test-report.html";
|
|
}
|
|
});
|
|
reportParts.add("projectId", projectId.toString());
|
|
reportParts.add("fileType", "HTML");
|
|
|
|
HttpHeaders reportHeaders = new HttpHeaders();
|
|
reportHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
|
|
HttpEntity<MultiValueMap<String, Object>> reportRequest = new HttpEntity<>(reportParts, reportHeaders);
|
|
|
|
ResponseEntity<Map> uploadResponse = restTemplate.postForEntity(
|
|
baseUrl + "/api/reports",
|
|
reportRequest,
|
|
Map.class
|
|
);
|
|
|
|
assertEquals(HttpStatus.CREATED, uploadResponse.getStatusCode());
|
|
assertNotNull(uploadResponse.getBody());
|
|
Long reportId = ((Number) uploadResponse.getBody().get("id")).longValue();
|
|
assertNotNull(reportId);
|
|
assertEquals("test-report.html", uploadResponse.getBody().get("fileName"));
|
|
assertEquals("HTML", uploadResponse.getBody().get("fileType"));
|
|
String filePath = (String) uploadResponse.getBody().get("filePath");
|
|
System.out.println("[Step 3] Uploaded report with ID: " + reportId + ", path: " + filePath);
|
|
|
|
// Verify file was actually saved
|
|
Path savedFile = Paths.get(filePath);
|
|
assertTrue(Files.exists(savedFile), "File should exist on disk");
|
|
|
|
// Step 4: Query reports by projectId
|
|
ResponseEntity<List> reportsResponse = restTemplate.getForEntity(
|
|
baseUrl + "/api/reports?projectId=" + projectId,
|
|
List.class
|
|
);
|
|
assertEquals(HttpStatus.OK, reportsResponse.getStatusCode());
|
|
assertNotNull(reportsResponse.getBody());
|
|
assertEquals(1, reportsResponse.getBody().size());
|
|
Map reportInList = (Map) reportsResponse.getBody().get(0);
|
|
assertEquals(reportId, ((Number) reportInList.get("id")).longValue());
|
|
System.out.println("[Step 4] Queried reports, found: " + reportsResponse.getBody().size());
|
|
|
|
// Step 5: Get report by ID with content
|
|
ResponseEntity<Map> getReportResponse = restTemplate.getForEntity(
|
|
baseUrl + "/api/reports/" + reportId,
|
|
Map.class
|
|
);
|
|
assertEquals(HttpStatus.OK, getReportResponse.getStatusCode());
|
|
assertEquals("test-report.html", getReportResponse.getBody().get("fileName"));
|
|
assertEquals(htmlContent, getReportResponse.getBody().get("fileContent"));
|
|
System.out.println("[Step 5] Retrieved report with content");
|
|
|
|
// Step 6: Delete the report
|
|
ResponseEntity<Void> deleteReportResponse = restTemplate.exchange(
|
|
baseUrl + "/api/reports/" + reportId,
|
|
HttpMethod.DELETE,
|
|
null,
|
|
Void.class
|
|
);
|
|
assertEquals(HttpStatus.NO_CONTENT, deleteReportResponse.getStatusCode());
|
|
|
|
// Verify report file is deleted
|
|
assertFalse(Files.exists(savedFile), "File should be deleted from disk");
|
|
|
|
// Verify report is gone
|
|
ResponseEntity<Map> getDeletedReport = restTemplate.getForEntity(
|
|
baseUrl + "/api/reports/" + reportId,
|
|
Map.class
|
|
);
|
|
assertEquals(HttpStatus.NOT_FOUND, getDeletedReport.getStatusCode());
|
|
System.out.println("[Step 6] Deleted report");
|
|
|
|
// Step 7: Delete the project
|
|
ResponseEntity<Void> deleteProjectResponse = restTemplate.exchange(
|
|
baseUrl + "/api/projects/" + projectId,
|
|
HttpMethod.DELETE,
|
|
null,
|
|
Void.class
|
|
);
|
|
assertEquals(HttpStatus.NO_CONTENT, deleteProjectResponse.getStatusCode());
|
|
|
|
// Verify project is gone
|
|
ResponseEntity<Map> getDeletedProject = restTemplate.getForEntity(
|
|
baseUrl + "/api/projects/" + projectId,
|
|
Map.class
|
|
);
|
|
assertEquals(HttpStatus.NOT_FOUND, getDeletedProject.getStatusCode());
|
|
System.out.println("[Step 7] Deleted project");
|
|
|
|
System.out.println("[SUCCESS] Complete API flow test passed!");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Cascade delete: Deleting project should not delete reports (manual cleanup)")
|
|
void deleteProject_shouldNotAutoDeleteReports() {
|
|
// Create project
|
|
ProjectRequest projectRequest = new ProjectRequest("Cascade Test Project", "Testing cascade");
|
|
ResponseEntity<Map> createResponse = restTemplate.postForEntity(
|
|
baseUrl + "/api/projects",
|
|
projectRequest,
|
|
Map.class
|
|
);
|
|
assertEquals(HttpStatus.CREATED, createResponse.getStatusCode());
|
|
Long projectId = ((Number) createResponse.getBody().get("id")).longValue();
|
|
|
|
// Upload two reports
|
|
for (int i = 1; i <= 2; i++) {
|
|
final int reportIndex = i;
|
|
String content = "Report " + i;
|
|
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
|
parts.add("file", new ByteArrayResource(content.getBytes()) {
|
|
@Override
|
|
public String getFilename() {
|
|
return "report" + reportIndex + ".html";
|
|
}
|
|
});
|
|
parts.add("projectId", projectId.toString());
|
|
parts.add("fileType", "HTML");
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
|
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
|
|
|
ResponseEntity<Map> response = restTemplate.postForEntity(
|
|
baseUrl + "/api/reports",
|
|
request,
|
|
Map.class
|
|
);
|
|
assertEquals(HttpStatus.CREATED, response.getStatusCode());
|
|
}
|
|
|
|
// Verify reports exist
|
|
ResponseEntity<List> reportsResponse = restTemplate.getForEntity(
|
|
baseUrl + "/api/reports?projectId=" + projectId,
|
|
List.class
|
|
);
|
|
assertEquals(2, reportsResponse.getBody().size());
|
|
|
|
// Delete project
|
|
ResponseEntity<Void> deleteResponse = restTemplate.exchange(
|
|
baseUrl + "/api/projects/" + projectId,
|
|
HttpMethod.DELETE,
|
|
null,
|
|
Void.class
|
|
);
|
|
assertEquals(HttpStatus.NO_CONTENT, deleteResponse.getStatusCode());
|
|
|
|
// Project should be gone
|
|
ResponseEntity<Map> getProject = restTemplate.getForEntity(
|
|
baseUrl + "/api/projects/" + projectId,
|
|
Map.class
|
|
);
|
|
assertEquals(HttpStatus.NOT_FOUND, getProject.getStatusCode());
|
|
|
|
// Reports are cascade-deleted along with the project
|
|
ResponseEntity<List> remainingReports = restTemplate.getForEntity(
|
|
baseUrl + "/api/reports?projectId=" + projectId,
|
|
List.class
|
|
);
|
|
assertEquals(0, remainingReports.getBody().size());
|
|
System.out.println("[INFO] Project deleted. All reports cascade-deleted.");
|
|
}
|
|
} |