fix: FilePreview fileType case + Tailwind v4 gradient transparent bug

- FilePreview.vue: add normalizedFileType computed to handle backend
  returning uppercase HTML/MD/PPTX (fixes preview/download buttons)
- FilePreview.vue: bg-gradient-to-r from-orange-500 -> bg-orange-500
  (Tailwind v4 gradient + CSS variable = transparent)
- ReportCard.vue: bg-gradient-to-r -> bg-orange-600 for selected state
- Add .opencode/, node_modules/, dist/ to .gitignore
- Initial git setup for publish project
This commit is contained in:
2026-05-24 20:09:42 +08:00
commit b9137204a0
78 changed files with 12950 additions and 0 deletions
@@ -0,0 +1,252 @@
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 still exist (they're orphaned - not cascade deleted)
// This confirms reports are independent entities
ResponseEntity<List> orphanedReports = restTemplate.getForEntity(
baseUrl + "/api/reports",
List.class
);
assertTrue(orphanedReports.getBody().size() >= 2);
System.out.println("[INFO] Project deleted. Reports remain in database (manual cleanup required).");
}
}