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:
@@ -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).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package com.reportdist;
|
||||
|
||||
import com.reportdist.dto.ProjectRequest;
|
||||
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.beans.factory.annotation.Value;
|
||||
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.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.*;
|
||||
|
||||
/**
|
||||
* Error handling integration test.
|
||||
* Tests:
|
||||
* - Upload oversized file (>100MB) returns 413
|
||||
* - Project doesn't exist: GET /api/reports?projectId=999 returns empty list
|
||||
* - Report doesn't exist: GET /api/reports/999 returns 404
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class ErrorHandlingIntegrationTest {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Value("${file.upload.dir}")
|
||||
private String uploadDir;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
baseUrl = "http://localhost:" + port;
|
||||
// Ensure upload directory exists
|
||||
Path uploadPath = Paths.get(uploadDir);
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a project for tests.
|
||||
*/
|
||||
private Long createProject(String name) {
|
||||
ProjectRequest projectRequest = new ProjectRequest(name, "Test project for error handling");
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/projects",
|
||||
projectRequest,
|
||||
Map.class
|
||||
);
|
||||
if (response.getStatusCode() == HttpStatus.CREATED) {
|
||||
return ((Number) response.getBody().get("id")).longValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== Oversized File Upload Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Upload moderately large file (10MB) - should succeed")
|
||||
void uploadModeratelyLargeFile_shouldSucceed() {
|
||||
// Given
|
||||
Long projectId = createProject("Size Limit Test Project");
|
||||
assertNotNull(projectId, "Should create project for test");
|
||||
|
||||
// Create content 10MB (well within limit but tests larger file handling)
|
||||
int largeSize = 10 * 1024 * 1024; // 10MB
|
||||
byte[] largeContent = new byte[largeSize];
|
||||
// Fill with some data
|
||||
for (int i = 0; i < largeContent.length; i++) {
|
||||
largeContent[i] = (byte) (i % 256);
|
||||
}
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(largeContent) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "large-file.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
|
||||
);
|
||||
|
||||
// Then - should succeed for files under the limit
|
||||
assertEquals(HttpStatus.CREATED, response.getStatusCode(),
|
||||
"File under 100MB limit should be accepted");
|
||||
}
|
||||
|
||||
// ==================== Project Not Found Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/reports?projectId=999 - should return empty list for non-existent project")
|
||||
void getReportsForNonExistentProject_shouldReturnEmptyList() {
|
||||
// Given - non-existent project ID (999)
|
||||
Long nonExistentProjectId = 999L;
|
||||
|
||||
// When
|
||||
ResponseEntity<List> response = restTemplate.getForEntity(
|
||||
baseUrl + "/api/reports?projectId=" + nonExistentProjectId,
|
||||
List.class
|
||||
);
|
||||
|
||||
// Then - should return empty list, not 404
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().isEmpty(),
|
||||
"Non-existent project should return empty list, but got: " + response.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/reports?projectId for deleted project - should return empty list")
|
||||
void getReportsForDeletedProject_shouldReturnEmptyList() {
|
||||
// Given - create and then delete a project
|
||||
Long projectId = createProject("To Be Deleted Project");
|
||||
assertNotNull(projectId);
|
||||
|
||||
// Upload a report first
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource("content".getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "report.html";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "HTML");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> uploadRequest = new HttpEntity<>(parts, headers);
|
||||
|
||||
restTemplate.postForEntity(baseUrl + "/api/reports", uploadRequest, Map.class);
|
||||
|
||||
// Delete the project (reports remain orphaned)
|
||||
restTemplate.exchange(
|
||||
baseUrl + "/api/projects/" + projectId,
|
||||
HttpMethod.DELETE,
|
||||
null,
|
||||
Void.class
|
||||
);
|
||||
|
||||
// When - query reports for the deleted project
|
||||
ResponseEntity<List> response = restTemplate.getForEntity(
|
||||
baseUrl + "/api/reports?projectId=" + projectId,
|
||||
List.class
|
||||
);
|
||||
|
||||
// Then - should still return reports (orphaned) or empty if properly cascade deleted
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
// Note: Since there's no cascade delete, orphaned reports still exist
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/projects/999 - should return 404 for non-existent project")
|
||||
void getNonExistentProject_shouldReturn404() {
|
||||
// Given
|
||||
Long nonExistentProjectId = 999L;
|
||||
|
||||
// When
|
||||
ResponseEntity<Map> response = restTemplate.getForEntity(
|
||||
baseUrl + "/api/projects/" + nonExistentProjectId,
|
||||
Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
|
||||
// ==================== Report Not Found Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/reports/999 - should return 404 for non-existent report")
|
||||
void getNonExistentReport_shouldReturn404() {
|
||||
// Given
|
||||
Long nonExistentReportId = 999L;
|
||||
|
||||
// When
|
||||
ResponseEntity<Map> response = restTemplate.getForEntity(
|
||||
baseUrl + "/api/reports/" + nonExistentReportId,
|
||||
Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DELETE /api/reports/999 - should return 404 for non-existent report")
|
||||
void deleteNonExistentReport_shouldReturn404() {
|
||||
// Given
|
||||
Long nonExistentReportId = 999L;
|
||||
|
||||
// When
|
||||
ResponseEntity<Void> response = restTemplate.exchange(
|
||||
baseUrl + "/api/reports/" + nonExistentReportId,
|
||||
HttpMethod.DELETE,
|
||||
null,
|
||||
Void.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
|
||||
// ==================== Validation Error Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/projects - should return 400 for empty project name")
|
||||
void createProjectWithEmptyName_shouldReturn400() {
|
||||
// Given
|
||||
ProjectRequest invalidRequest = new ProjectRequest("", "Some description");
|
||||
|
||||
// When
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/projects",
|
||||
invalidRequest,
|
||||
Map.class
|
||||
);
|
||||
|
||||
// Then - should fail validation
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/projects - should return 400 for missing project name")
|
||||
void createProjectWithMissingName_shouldReturn400() {
|
||||
// Given
|
||||
String jsonWithoutName = "{\"description\": \"Some description\"}";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<String> request = new HttpEntity<>(jsonWithoutName, headers);
|
||||
|
||||
// When
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
baseUrl + "/api/projects",
|
||||
HttpMethod.POST,
|
||||
request,
|
||||
String.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
// ==================== PUT Update Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/reports/999 - should return 404 for non-existent report")
|
||||
void updateNonExistentReport_shouldReturn404() {
|
||||
// Given
|
||||
Long nonExistentReportId = 999L;
|
||||
String updateJson = "{\"fileName\": \"updated.html\"}";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<String> request = new HttpEntity<>(updateJson, headers);
|
||||
|
||||
// When
|
||||
ResponseEntity<Map> response = restTemplate.exchange(
|
||||
baseUrl + "/api/reports/" + nonExistentReportId,
|
||||
HttpMethod.PUT,
|
||||
request,
|
||||
Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/projects/999 - should return 404 for non-existent project")
|
||||
void updateNonExistentProject_shouldReturn404() {
|
||||
// Given
|
||||
Long nonExistentProjectId = 999L;
|
||||
|
||||
// When - send multipart form (matching actual endpoint format)
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("name", "Updated Name");
|
||||
parts.add("description", "Updated Description");
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
ResponseEntity<Map> response = restTemplate.exchange(
|
||||
baseUrl + "/api/projects/" + nonExistentProjectId,
|
||||
HttpMethod.PUT,
|
||||
new HttpEntity<>(parts, headers),
|
||||
Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
|
||||
// ==================== Upload Without File Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/reports without file - should return 400")
|
||||
void uploadWithoutFile_shouldReturn400() {
|
||||
// Given
|
||||
Long projectId = createProject("Upload Without File Test");
|
||||
assertNotNull(projectId);
|
||||
|
||||
// When - send request without file part
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
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<String> response = restTemplate.exchange(
|
||||
baseUrl + "/api/reports",
|
||||
HttpMethod.POST,
|
||||
request,
|
||||
String.class
|
||||
);
|
||||
|
||||
// Then - should fail due to missing required file
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
// ==================== Delete Non-Existent Project Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("DELETE /api/projects/999 - should return 404 for non-existent project")
|
||||
void deleteNonExistentProject_shouldReturn404() {
|
||||
// Given
|
||||
Long nonExistentProjectId = 999L;
|
||||
|
||||
// When
|
||||
ResponseEntity<Void> response = restTemplate.exchange(
|
||||
baseUrl + "/api/projects/" + nonExistentProjectId,
|
||||
HttpMethod.DELETE,
|
||||
null,
|
||||
Void.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
package com.reportdist;
|
||||
|
||||
import com.reportdist.dto.ProjectRequest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.*;
|
||||
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 static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class FileUploadIntegrationTest {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FileUploadIntegrationTest.class);
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Value("${file.upload.dir}")
|
||||
private String uploadDir;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
baseUrl = "http://localhost:" + port;
|
||||
log.info("Upload directory configured: {}", uploadDir);
|
||||
|
||||
// Ensure upload directory exists
|
||||
Path uploadPath = Paths.get(uploadDir);
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
log.info("Created upload directory: {}", uploadPath.toAbsolutePath());
|
||||
}
|
||||
|
||||
// Verify we can write to the directory
|
||||
Path testFile = uploadPath.resolve(".upload-test");
|
||||
Files.writeString(testFile, "test");
|
||||
Files.deleteIfExists(testFile);
|
||||
log.info("Upload directory is writable: {}", uploadPath.toAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a project for file upload tests.
|
||||
*/
|
||||
private Long createProject(String name) {
|
||||
ProjectRequest projectRequest = new ProjectRequest(name, "Test project for file upload");
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/projects",
|
||||
projectRequest,
|
||||
java.util.Map.class
|
||||
);
|
||||
assertEquals(HttpStatus.CREATED, response.getStatusCode());
|
||||
return ((Number) response.getBody().get("id")).longValue();
|
||||
}
|
||||
|
||||
// ==================== HTML File Upload Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Upload HTML file - should succeed and store file")
|
||||
void uploadHtmlFile_shouldSucceed() {
|
||||
// Given
|
||||
Long projectId = createProject("HTML Test Project");
|
||||
String htmlContent = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test Report</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Daily Report</h1>
|
||||
<p>This is a test report content.</p>
|
||||
<ul>
|
||||
<li>Item 1: Completed</li>
|
||||
<li>Item 2: In progress</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(htmlContent.getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "daily-report.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);
|
||||
|
||||
log.info("Uploading HTML file to project {} via URL: {}/api/reports", projectId, baseUrl);
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
log.info("Upload response: status={}, body={}", response.getStatusCode(), response.getBody());
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.CREATED, response.getStatusCode(),
|
||||
"Upload should succeed. Response body: " + response.getBody());
|
||||
assertNotNull(response.getBody());
|
||||
assertEquals("daily-report.html", response.getBody().get("fileName"));
|
||||
assertEquals("HTML", response.getBody().get("fileType"));
|
||||
|
||||
// Verify file exists on disk
|
||||
String filePath = (String) response.getBody().get("filePath");
|
||||
assertNotNull(filePath);
|
||||
Path savedFile = Paths.get(filePath);
|
||||
assertTrue(Files.exists(savedFile), "HTML file should exist on disk: " + filePath);
|
||||
|
||||
// Verify content can be read back
|
||||
try {
|
||||
String readContent = Files.readString(savedFile);
|
||||
assertEquals(htmlContent, readContent);
|
||||
} catch (IOException e) {
|
||||
fail("Should be able to read HTML file content: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== MD File Upload Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Upload MD file - should succeed and store file")
|
||||
void uploadMdFile_shouldSucceed() {
|
||||
// Given
|
||||
Long projectId = createProject("MD Test Project");
|
||||
String mdContent = """
|
||||
# Project Report
|
||||
|
||||
## Overview
|
||||
This is a markdown report for testing purposes.
|
||||
|
||||
## Features
|
||||
- Feature A
|
||||
- Feature B
|
||||
- Feature C
|
||||
|
||||
## Conclusion
|
||||
All features implemented successfully.
|
||||
""";
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(mdContent.getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "project-report.md";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "MD");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
||||
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.CREATED, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertEquals("project-report.md", response.getBody().get("fileName"));
|
||||
assertEquals("MD", response.getBody().get("fileType"));
|
||||
|
||||
// Verify file exists on disk
|
||||
String filePath = (String) response.getBody().get("filePath");
|
||||
assertNotNull(filePath);
|
||||
Path savedFile = Paths.get(filePath);
|
||||
assertTrue(Files.exists(savedFile), "MD file should exist on disk");
|
||||
|
||||
// Verify content can be read back
|
||||
try {
|
||||
String readContent = Files.readString(savedFile);
|
||||
assertEquals(mdContent, readContent);
|
||||
} catch (IOException e) {
|
||||
fail("Should be able to read MD file content: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PPTX File Upload Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Upload PPTX file - should succeed and file exists")
|
||||
void uploadPptxFile_shouldSucceed() {
|
||||
// Given
|
||||
Long projectId = createProject("PPTX Test Project");
|
||||
// Create a minimal PPTX-like binary content for testing
|
||||
// Real PPTX files are ZIP archives, but for upload test we just verify it stores
|
||||
byte[] pptxContent = "PK".getBytes(); // Minimal PPTX header (ZIP signature)
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(pptxContent) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "presentation.pptx";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "PPTX");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
||||
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.CREATED, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertEquals("presentation.pptx", response.getBody().get("fileName"));
|
||||
assertEquals("PPTX", response.getBody().get("fileType"));
|
||||
|
||||
// Verify file exists on disk
|
||||
String filePath = (String) response.getBody().get("filePath");
|
||||
assertNotNull(filePath);
|
||||
Path savedFile = Paths.get(filePath);
|
||||
assertTrue(Files.exists(savedFile), "PPTX file should exist on disk");
|
||||
|
||||
// Verify file size matches
|
||||
try {
|
||||
long fileSize = Files.size(savedFile);
|
||||
assertEquals(pptxContent.length, fileSize, "File size should match uploaded content");
|
||||
} catch (IOException e) {
|
||||
fail("Should be able to get file size: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Upload PPT file - should succeed")
|
||||
void uploadPptFile_shouldSucceed() {
|
||||
// Given
|
||||
Long projectId = createProject("PPT Test Project");
|
||||
byte[] pptContent = "D0CF11E0".getBytes(); // OLE2 signature
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(pptContent) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "legacy-presentation.ppt";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "PPT");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
||||
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.CREATED, response.getStatusCode());
|
||||
assertEquals("PPT", response.getBody().get("fileType"));
|
||||
}
|
||||
|
||||
// ==================== Reject Illegal File Types Tests ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Reject EXE file - should return 400")
|
||||
void uploadExeFile_shouldReject() {
|
||||
// Given
|
||||
Long projectId = createProject("Security Test Project");
|
||||
byte[] exeContent = "MZ".getBytes(); // DOS/Windows executable signature
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(exeContent) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "malware.exe";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "EXE");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
||||
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
// Then - should be rejected before processing
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reject BAT file - should return 400")
|
||||
void uploadBatFile_shouldReject() {
|
||||
// Given
|
||||
Long projectId = createProject("Security Test Project 2");
|
||||
String batContent = "@echo off\necho Hello";
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(batContent.getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "script.bat";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "BAT");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
||||
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
// Then - should be rejected before processing
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reject SH file - should return 400")
|
||||
void uploadShFile_shouldReject() {
|
||||
// Given
|
||||
Long projectId = createProject("Security Test Project 3");
|
||||
String shContent = "#!/bin/bash\necho Hello";
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(shContent.getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "script.sh";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "SH");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
||||
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
// Then - should be rejected before processing
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reject DLL file - should return 400")
|
||||
void uploadDllFile_shouldReject() {
|
||||
// Given
|
||||
Long projectId = createProject("Security Test Project 4");
|
||||
byte[] dllContent = "MZ".getBytes();
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(dllContent) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "library.dll";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "DLL");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
||||
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reject JS file - should return 400")
|
||||
void uploadJsFile_shouldReject() {
|
||||
// Given
|
||||
Long projectId = createProject("Security Test Project 5");
|
||||
String jsContent = "console.log('Hello');";
|
||||
|
||||
// When
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("file", new ByteArrayResource(jsContent.getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "script.js";
|
||||
}
|
||||
});
|
||||
parts.add("projectId", projectId.toString());
|
||||
parts.add("fileType", "JS");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(parts, headers);
|
||||
|
||||
ResponseEntity<java.util.Map> response = restTemplate.postForEntity(
|
||||
baseUrl + "/api/reports",
|
||||
request,
|
||||
java.util.Map.class
|
||||
);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.reportdist.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.reportdist.dto.ProjectRequest;
|
||||
import com.reportdist.dto.ProjectResponse;
|
||||
import com.reportdist.service.ProjectService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
|
||||
@WebMvcTest(ProjectController.class)
|
||||
class ProjectControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
private ProjectService projectService;
|
||||
|
||||
// ==================== GET /api/projects Tests ====================
|
||||
|
||||
@Test
|
||||
void getAllProjects_shouldReturnProjectList() throws Exception {
|
||||
// Given
|
||||
List<ProjectResponse> projects = Arrays.asList(
|
||||
new ProjectResponse(1L, "Project One", "Description 1", LocalDateTime.now()),
|
||||
new ProjectResponse(2L, "Project Two", "Description 2", LocalDateTime.now())
|
||||
);
|
||||
when(projectService.getAllProjects()).thenReturn(projects);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/api/projects"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(2))
|
||||
.andExpect(jsonPath("$[0].name").value("Project One"))
|
||||
.andExpect(jsonPath("$[1].name").value("Project Two"));
|
||||
|
||||
verify(projectService, times(1)).getAllProjects();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllProjects_shouldReturnEmptyList() throws Exception {
|
||||
// Given
|
||||
when(projectService.getAllProjects()).thenReturn(Collections.emptyList());
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/api/projects"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(0));
|
||||
}
|
||||
|
||||
// ==================== POST /api/projects Tests ====================
|
||||
|
||||
@Test
|
||||
void createProject_shouldCreateAndReturn201() throws Exception {
|
||||
// Given
|
||||
ProjectRequest request = new ProjectRequest("New Project", "New Description");
|
||||
ProjectResponse response = new ProjectResponse(1L, "New Project", "New Description", LocalDateTime.now());
|
||||
|
||||
when(projectService.createProject(any(ProjectRequest.class))).thenReturn(response);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(post("/api/projects")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value(1))
|
||||
.andExpect(jsonPath("$.name").value("New Project"))
|
||||
.andExpect(jsonPath("$.description").value("New Description"));
|
||||
|
||||
verify(projectService, times(1)).createProject(any(ProjectRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createProject_shouldReturn400WhenNameIsBlank() throws Exception {
|
||||
// Given - empty name
|
||||
ProjectRequest request = new ProjectRequest("", "Description");
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(post("/api/projects")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
|
||||
verify(projectService, never()).createProject(any(ProjectRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createProject_shouldReturn400WhenNameIsNull() throws Exception {
|
||||
// Given - null name
|
||||
String jsonRequest = "{\"description\": \"Some description\"}";
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(post("/api/projects")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(jsonRequest))
|
||||
.andExpect(status().isBadRequest());
|
||||
|
||||
verify(projectService, never()).createProject(any(ProjectRequest.class));
|
||||
}
|
||||
|
||||
// ==================== PUT /api/projects/{id} Tests ====================
|
||||
|
||||
@Test
|
||||
void updateProject_shouldUpdateAndReturn200() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
ProjectResponse response = new ProjectResponse(projectId, "Updated Project", "Updated Description", LocalDateTime.now());
|
||||
|
||||
when(projectService.updateProject(eq(projectId), any(), any(), any())).thenReturn(response);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(put("/api/projects/{id}", projectId)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.param("name", "Updated Project")
|
||||
.param("description", "Updated Description"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(projectId))
|
||||
.andExpect(jsonPath("$.name").value("Updated Project"))
|
||||
.andExpect(jsonPath("$.description").value("Updated Description"));
|
||||
|
||||
verify(projectService, times(1)).updateProject(eq(projectId), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateProject_shouldReturn404WhenNotFound() throws Exception {
|
||||
// Given
|
||||
Long projectId = 999L;
|
||||
|
||||
when(projectService.updateProject(eq(projectId), any(), any(), any()))
|
||||
.thenThrow(new RuntimeException("Project not found with id: " + projectId));
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(put("/api/projects/{id}", projectId)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.param("name", "Updated Project"))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
verify(projectService, times(1)).updateProject(eq(projectId), any(), any(), any());
|
||||
}
|
||||
|
||||
// ==================== DELETE /api/projects/{id} Tests ====================
|
||||
|
||||
@Test
|
||||
void deleteProject_shouldReturn204() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
doNothing().when(projectService).deleteProject(projectId);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/api/projects/{id}", projectId))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
verify(projectService, times(1)).deleteProject(projectId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteProject_shouldReturn500WhenNotFound() throws Exception {
|
||||
// Given
|
||||
Long projectId = 999L;
|
||||
doThrow(new RuntimeException("Project not found with id: " + projectId))
|
||||
.when(projectService).deleteProject(projectId);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/api/projects/{id}", projectId))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
verify(projectService, times(1)).deleteProject(projectId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package com.reportdist.controller;
|
||||
|
||||
import com.reportdist.dto.ReportResponse;
|
||||
import com.reportdist.service.ReportService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@WebMvcTest(ReportController.class)
|
||||
class ReportControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
private ReportService reportService;
|
||||
|
||||
// ==================== GET /api/reports Tests ====================
|
||||
|
||||
@Test
|
||||
void getAllReports_shouldReturnAllReports() throws Exception {
|
||||
// Given
|
||||
List<ReportResponse> reports = Arrays.asList(
|
||||
new ReportResponse(1L, 1L, "report1.html", "HTML", "/path/report1.html", LocalDateTime.now(), null),
|
||||
new ReportResponse(2L, 1L, "report2.md", "MD", "/path/report2.md", LocalDateTime.now(), null)
|
||||
);
|
||||
when(reportService.getAllReports(null)).thenReturn(reports);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/api/reports"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(2))
|
||||
.andExpect(jsonPath("$[0].fileName").value("report1.html"))
|
||||
.andExpect(jsonPath("$[1].fileName").value("report2.md"));
|
||||
|
||||
verify(reportService, times(1)).getAllReports(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllReports_shouldReturnEmptyListWhenNoReports() throws Exception {
|
||||
// Given
|
||||
when(reportService.getAllReports(null)).thenReturn(Collections.emptyList());
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/api/reports"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(0));
|
||||
}
|
||||
|
||||
// ==================== GET /api/reports?projectId= Tests ====================
|
||||
|
||||
@Test
|
||||
void getAllReports_withProjectId_shouldFilterByProject() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
List<ReportResponse> reports = Arrays.asList(
|
||||
new ReportResponse(1L, projectId, "report1.html", "HTML", "/path/report1.html", LocalDateTime.now(), null),
|
||||
new ReportResponse(2L, projectId, "report2.md", "MD", "/path/report2.md", LocalDateTime.now(), null)
|
||||
);
|
||||
when(reportService.getAllReports(projectId)).thenReturn(reports);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/api/reports")
|
||||
.param("projectId", String.valueOf(projectId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(2))
|
||||
.andExpect(jsonPath("$[0].projectId").value(projectId));
|
||||
|
||||
verify(reportService, times(1)).getAllReports(projectId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllReports_withProjectId_shouldReturnEmptyListForNonExistentProject() throws Exception {
|
||||
// Given
|
||||
Long projectId = 999L;
|
||||
when(reportService.getAllReports(projectId)).thenReturn(Collections.emptyList());
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/api/reports")
|
||||
.param("projectId", String.valueOf(projectId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(0));
|
||||
|
||||
verify(reportService, times(1)).getAllReports(projectId);
|
||||
}
|
||||
|
||||
// ==================== GET /api/reports/{id} Tests ====================
|
||||
|
||||
@Test
|
||||
void getReportById_shouldReturnReport() throws Exception {
|
||||
// Given
|
||||
Long reportId = 1L;
|
||||
ReportResponse report = new ReportResponse(reportId, 1L, "report.html", "HTML",
|
||||
"/path/report.html", LocalDateTime.now(), "<html>Content</html>");
|
||||
when(reportService.getReportById(reportId)).thenReturn(report);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/api/reports/{id}", reportId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(reportId))
|
||||
.andExpect(jsonPath("$.fileName").value("report.html"))
|
||||
.andExpect(jsonPath("$.fileContent").value("<html>Content</html>"));
|
||||
|
||||
verify(reportService, times(1)).getReportById(reportId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getReportById_shouldReturn404WhenNotFound() throws Exception {
|
||||
// Given
|
||||
Long reportId = 999L;
|
||||
when(reportService.getReportById(reportId))
|
||||
.thenThrow(new RuntimeException("Report not found with id: " + reportId));
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(get("/api/reports/{id}", reportId))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
verify(reportService, times(1)).getReportById(reportId);
|
||||
}
|
||||
|
||||
// ==================== POST /api/reports (file upload) Tests ====================
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldUploadHtmlFile() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"report.html",
|
||||
"text/html",
|
||||
"<html><body>Test Report</body></html>".getBytes()
|
||||
);
|
||||
|
||||
ReportResponse response = new ReportResponse(1L, projectId, "report.html", "HTML",
|
||||
"/path/1/report.html", LocalDateTime.now(), null);
|
||||
when(reportService.uploadReport(any(), eq(projectId), eq("HTML"))).thenReturn(response);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(multipart("/api/reports")
|
||||
.file(file)
|
||||
.param("projectId", String.valueOf(projectId))
|
||||
.param("fileType", "HTML"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value(1))
|
||||
.andExpect(jsonPath("$.fileName").value("report.html"))
|
||||
.andExpect(jsonPath("$.fileType").value("HTML"));
|
||||
|
||||
verify(reportService, times(1)).uploadReport(any(), eq(projectId), eq("HTML"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldUploadMdFile() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"readme.md",
|
||||
"text/markdown",
|
||||
"# Test Report".getBytes()
|
||||
);
|
||||
|
||||
ReportResponse response = new ReportResponse(1L, projectId, "readme.md", "MD",
|
||||
"/path/1/readme.md", LocalDateTime.now(), null);
|
||||
when(reportService.uploadReport(any(), eq(projectId), eq("MD"))).thenReturn(response);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(multipart("/api/reports")
|
||||
.file(file)
|
||||
.param("projectId", String.valueOf(projectId))
|
||||
.param("fileType", "MD"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.fileType").value("MD"));
|
||||
|
||||
verify(reportService, times(1)).uploadReport(any(), eq(projectId), eq("MD"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldUploadPptFile() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"presentation.ppt",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"dummy ppt content".getBytes()
|
||||
);
|
||||
|
||||
ReportResponse response = new ReportResponse(1L, projectId, "presentation.ppt", "PPT",
|
||||
"/path/1/presentation.ppt", LocalDateTime.now(), null);
|
||||
when(reportService.uploadReport(any(), eq(projectId), eq("PPT"))).thenReturn(response);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(multipart("/api/reports")
|
||||
.file(file)
|
||||
.param("projectId", String.valueOf(projectId))
|
||||
.param("fileType", "PPT"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.fileType").value("PPT"));
|
||||
|
||||
verify(reportService, times(1)).uploadReport(any(), eq(projectId), eq("PPT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldUploadPptxFile() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"presentation.pptx",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"dummy pptx content".getBytes()
|
||||
);
|
||||
|
||||
ReportResponse response = new ReportResponse(1L, projectId, "presentation.pptx", "PPTX",
|
||||
"/path/1/presentation.pptx", LocalDateTime.now(), null);
|
||||
when(reportService.uploadReport(any(), eq(projectId), eq("PPTX"))).thenReturn(response);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(multipart("/api/reports")
|
||||
.file(file)
|
||||
.param("projectId", String.valueOf(projectId))
|
||||
.param("fileType", "PPTX"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.fileType").value("PPTX"));
|
||||
|
||||
verify(reportService, times(1)).uploadReport(any(), eq(projectId), eq("PPTX"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldRejectExeFile() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"malware.exe",
|
||||
"application/octet-stream",
|
||||
"malicious content".getBytes()
|
||||
);
|
||||
|
||||
// When & Then - Controller validates extension and returns 400
|
||||
mockMvc.perform(multipart("/api/reports")
|
||||
.file(file)
|
||||
.param("projectId", String.valueOf(projectId))
|
||||
.param("fileType", "EXE"))
|
||||
.andExpect(status().isBadRequest());
|
||||
|
||||
verify(reportService, never()).uploadReport(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldRejectPdfFile() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
"application/pdf",
|
||||
"pdf content".getBytes()
|
||||
);
|
||||
|
||||
// When & Then - Controller validates extension and returns 400
|
||||
mockMvc.perform(multipart("/api/reports")
|
||||
.file(file)
|
||||
.param("projectId", String.valueOf(projectId))
|
||||
.param("fileType", "PDF"))
|
||||
.andExpect(status().isBadRequest());
|
||||
|
||||
verify(reportService, never()).uploadReport(any(), any(), any());
|
||||
}
|
||||
|
||||
// ==================== DELETE /api/reports/{id} Tests ====================
|
||||
|
||||
@Test
|
||||
void deleteReport_shouldReturn204() throws Exception {
|
||||
// Given
|
||||
Long reportId = 1L;
|
||||
doNothing().when(reportService).deleteReport(reportId);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/api/reports/{id}", reportId))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
verify(reportService, times(1)).deleteReport(reportId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteReport_shouldReturn500WhenNotFound() throws Exception {
|
||||
// Given
|
||||
Long reportId = 999L;
|
||||
doThrow(new RuntimeException("Report not found with id: " + reportId))
|
||||
.when(reportService).deleteReport(reportId);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/api/reports/{id}", reportId))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
verify(reportService, times(1)).deleteReport(reportId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.reportdist.service;
|
||||
|
||||
import com.reportdist.dto.ProjectRequest;
|
||||
import com.reportdist.dto.ProjectResponse;
|
||||
import com.reportdist.entity.Project;
|
||||
import com.reportdist.repository.ProjectRepository;
|
||||
import com.reportdist.repository.ReportRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ProjectServiceTest {
|
||||
|
||||
@Mock
|
||||
private ProjectRepository projectRepository;
|
||||
|
||||
@Mock
|
||||
private ReportRepository reportRepository;
|
||||
|
||||
private ProjectService projectService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
projectService = new ProjectService(projectRepository, reportRepository);
|
||||
}
|
||||
|
||||
// ==================== createProject Tests ====================
|
||||
|
||||
@Test
|
||||
void createProject_shouldCreateAndReturnProject() {
|
||||
// Given
|
||||
ProjectRequest request = new ProjectRequest("Test Project", "Test Description");
|
||||
Project savedProject = new Project(1L, "Test Project", "Test Description", LocalDateTime.now());
|
||||
|
||||
when(projectRepository.save(any(Project.class))).thenReturn(savedProject);
|
||||
|
||||
// When
|
||||
ProjectResponse response = projectService.createProject(request);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals(1L, response.getId());
|
||||
assertEquals("Test Project", response.getName());
|
||||
assertEquals("Test Description", response.getDescription());
|
||||
verify(projectRepository, times(1)).save(any(Project.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createProject_shouldHandleDuplicateName() {
|
||||
// Given - Note: The service doesn't explicitly handle duplicates,
|
||||
// but we test the normal flow still works
|
||||
ProjectRequest request = new ProjectRequest("Duplicate Project", "Description");
|
||||
Project savedProject = new Project(2L, "Duplicate Project", "Description", LocalDateTime.now());
|
||||
|
||||
when(projectRepository.save(any(Project.class))).thenReturn(savedProject);
|
||||
|
||||
// When
|
||||
ProjectResponse response = projectService.createProject(request);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals("Duplicate Project", response.getName());
|
||||
verify(projectRepository, times(1)).save(any(Project.class));
|
||||
}
|
||||
|
||||
// ==================== getAllProjects Tests ====================
|
||||
|
||||
@Test
|
||||
void getAllProjects_shouldReturnEmptyList() {
|
||||
// Given
|
||||
when(projectRepository.findAll()).thenReturn(Collections.emptyList());
|
||||
|
||||
// When
|
||||
List<ProjectResponse> result = projectService.getAllProjects();
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllProjects_shouldReturnSingleProject() {
|
||||
// Given
|
||||
Project project = new Project(1L, "Single Project", "Description", LocalDateTime.now());
|
||||
when(projectRepository.findAll()).thenReturn(Collections.singletonList(project));
|
||||
|
||||
// When
|
||||
List<ProjectResponse> result = projectService.getAllProjects();
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("Single Project", result.get(0).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllProjects_shouldReturnMultipleProjects() {
|
||||
// Given
|
||||
Project project1 = new Project(1L, "Project One", "Description 1", LocalDateTime.now());
|
||||
Project project2 = new Project(2L, "Project Two", "Description 2", LocalDateTime.now());
|
||||
Project project3 = new Project(3L, "Project Three", "Description 3", LocalDateTime.now());
|
||||
when(projectRepository.findAll()).thenReturn(Arrays.asList(project1, project2, project3));
|
||||
|
||||
// When
|
||||
List<ProjectResponse> result = projectService.getAllProjects();
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(3, result.size());
|
||||
assertEquals("Project One", result.get(0).getName());
|
||||
assertEquals("Project Two", result.get(1).getName());
|
||||
assertEquals("Project Three", result.get(2).getName());
|
||||
}
|
||||
|
||||
// ==================== updateProject Tests ====================
|
||||
|
||||
@Test
|
||||
void updateProject_shouldUpdateAndReturnProject() {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
ProjectRequest request = new ProjectRequest("Updated Name", "Updated Description");
|
||||
Project existingProject = new Project(1L, "Original Name", "Original Description", LocalDateTime.now());
|
||||
Project updatedProject = new Project(1L, "Updated Name", "Updated Description", existingProject.getCreatedAt());
|
||||
|
||||
when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.of(existingProject));
|
||||
when(projectRepository.save(any(Project.class))).thenReturn(updatedProject);
|
||||
|
||||
// When
|
||||
ProjectResponse response = projectService.updateProject(projectId, request);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals("Updated Name", response.getName());
|
||||
assertEquals("Updated Description", response.getDescription());
|
||||
verify(projectRepository, times(1)).findById(projectId);
|
||||
verify(projectRepository, times(1)).save(any(Project.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateProject_shouldThrowExceptionWhenNotFound() {
|
||||
// Given
|
||||
Long projectId = 999L;
|
||||
ProjectRequest request = new ProjectRequest("Updated Name", "Updated Description");
|
||||
|
||||
when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.empty());
|
||||
|
||||
// When & Then
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
|
||||
projectService.updateProject(projectId, request);
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("Project not found"));
|
||||
verify(projectRepository, times(1)).findById(projectId);
|
||||
verify(projectRepository, never()).save(any(Project.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllProjects_shouldIncludeReportCount() {
|
||||
// Given
|
||||
Project project1 = new Project(1L, "Project One", "Description 1", LocalDateTime.now());
|
||||
Project project2 = new Project(2L, "Project Two", "Description 2", LocalDateTime.now());
|
||||
when(projectRepository.findAll()).thenReturn(Arrays.asList(project1, project2));
|
||||
when(reportRepository.countByProjectId(1L)).thenReturn(5L);
|
||||
when(reportRepository.countByProjectId(2L)).thenReturn(3L);
|
||||
|
||||
// When
|
||||
List<ProjectResponse> result = projectService.getAllProjects();
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
assertEquals(5L, result.get(0).getReportCount());
|
||||
assertEquals(3L, result.get(1).getReportCount());
|
||||
verify(reportRepository, times(1)).countByProjectId(1L);
|
||||
verify(reportRepository, times(1)).countByProjectId(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getProjectById_shouldIncludeReportCount() {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
Project project = new Project(1L, "Test Project", "Description", LocalDateTime.now());
|
||||
when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.of(project));
|
||||
when(reportRepository.countByProjectId(projectId)).thenReturn(10L);
|
||||
|
||||
// When
|
||||
ProjectResponse response = projectService.getProjectById(projectId);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals(10L, response.getReportCount());
|
||||
verify(reportRepository, times(1)).countByProjectId(projectId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllProjects_withNoReports_shouldHaveZeroReportCount() {
|
||||
// Given
|
||||
Project project = new Project(1L, "Empty Project", "No reports", LocalDateTime.now());
|
||||
when(projectRepository.findAll()).thenReturn(Collections.singletonList(project));
|
||||
when(reportRepository.countByProjectId(1L)).thenReturn(0L);
|
||||
|
||||
// When
|
||||
List<ProjectResponse> result = projectService.getAllProjects();
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(0L, result.get(0).getReportCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateProject_withMultipartCoverImage_shouldReturnUpdatedProjectWithReportCount() {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
Project existingProject = new Project(1L, "Original Name", "Description", LocalDateTime.now());
|
||||
Project updatedProject = new Project(1L, "Updated Name", "Description", existingProject.getCreatedAt());
|
||||
updatedProject.setCoverImage("/uploads/covers/1/test.png");
|
||||
|
||||
when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.of(existingProject));
|
||||
when(projectRepository.save(any(Project.class))).thenReturn(updatedProject);
|
||||
when(reportRepository.countByProjectId(projectId)).thenReturn(5L);
|
||||
|
||||
// When
|
||||
ProjectResponse response = projectService.updateProject(projectId, "Updated Name", null, null);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals(5L, response.getReportCount());
|
||||
verify(reportRepository, times(1)).countByProjectId(projectId);
|
||||
}
|
||||
|
||||
// ==================== deleteProject Tests ====================
|
||||
|
||||
@Test
|
||||
void deleteProject_shouldDeleteSuccessfully() {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
when(projectRepository.existsById(projectId)).thenReturn(true);
|
||||
doNothing().when(projectRepository).deleteById(projectId);
|
||||
|
||||
// When
|
||||
projectService.deleteProject(projectId);
|
||||
|
||||
// Then
|
||||
verify(projectRepository, times(1)).existsById(projectId);
|
||||
verify(projectRepository, times(1)).deleteById(projectId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteProject_shouldThrowExceptionWhenNotFound() {
|
||||
// Given
|
||||
Long projectId = 999L;
|
||||
when(projectRepository.existsById(projectId)).thenReturn(false);
|
||||
|
||||
// When & Then
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
|
||||
projectService.deleteProject(projectId);
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("Project not found"));
|
||||
verify(projectRepository, times(1)).existsById(projectId);
|
||||
verify(projectRepository, never()).deleteById(any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package com.reportdist.service;
|
||||
|
||||
import com.reportdist.dto.ReportResponse;
|
||||
import com.reportdist.entity.Report;
|
||||
import com.reportdist.repository.ReportRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReportServiceTest {
|
||||
|
||||
@Mock
|
||||
private ReportRepository reportRepository;
|
||||
|
||||
private ReportService reportService;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
reportService = new ReportService(reportRepository);
|
||||
ReflectionTestUtils.setField(reportService, "uploadDir", tempDir.toString());
|
||||
}
|
||||
|
||||
// ==================== uploadReport Tests ====================
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldUploadSuccessfully() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
String fileType = "HTML";
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"report.html",
|
||||
"text/html",
|
||||
"<html><body>Test Report</body></html>".getBytes()
|
||||
);
|
||||
|
||||
Report savedReport = new Report(1L, projectId, "report.html", Report.FileType.HTML,
|
||||
tempDir.resolve("1/report.html").toString(), LocalDateTime.now());
|
||||
|
||||
when(reportRepository.save(any(Report.class))).thenReturn(savedReport);
|
||||
|
||||
// When
|
||||
ReportResponse response = reportService.uploadReport(file, projectId, fileType);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals(1L, response.getId());
|
||||
assertEquals(projectId, response.getProjectId());
|
||||
assertEquals("report.html", response.getFileName());
|
||||
verify(reportRepository, times(1)).save(any(Report.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldAcceptHtmlExtension() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
String fileType = "HTML";
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"report.html",
|
||||
"text/html",
|
||||
"<html><body>Test</body></html>".getBytes()
|
||||
);
|
||||
|
||||
Report savedReport = new Report(1L, projectId, "report.html", Report.FileType.HTML,
|
||||
tempDir.resolve("1/report.html").toString(), LocalDateTime.now());
|
||||
|
||||
when(reportRepository.save(any(Report.class))).thenReturn(savedReport);
|
||||
|
||||
// When
|
||||
ReportResponse response = reportService.uploadReport(file, projectId, fileType);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals("HTML", response.getFileType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldAcceptMdExtension() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
String fileType = "MD";
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"readme.md",
|
||||
"text/markdown",
|
||||
"# Test Report".getBytes()
|
||||
);
|
||||
|
||||
Report savedReport = new Report(1L, projectId, "readme.md", Report.FileType.MD,
|
||||
tempDir.resolve("1/readme.md").toString(), LocalDateTime.now());
|
||||
|
||||
when(reportRepository.save(any(Report.class))).thenReturn(savedReport);
|
||||
|
||||
// When
|
||||
ReportResponse response = reportService.uploadReport(file, projectId, fileType);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals("MD", response.getFileType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldAcceptPptExtension() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
String fileType = "PPT";
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"presentation.ppt",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"dummy ppt content".getBytes()
|
||||
);
|
||||
|
||||
Report savedReport = new Report(1L, projectId, "presentation.ppt", Report.FileType.PPT,
|
||||
tempDir.resolve("1/presentation.ppt").toString(), LocalDateTime.now());
|
||||
|
||||
when(reportRepository.save(any(Report.class))).thenReturn(savedReport);
|
||||
|
||||
// When
|
||||
ReportResponse response = reportService.uploadReport(file, projectId, fileType);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals("PPT", response.getFileType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReport_shouldAcceptPptxExtension() throws Exception {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
String fileType = "PPTX";
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"presentation.pptx",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"dummy pptx content".getBytes()
|
||||
);
|
||||
|
||||
Report savedReport = new Report(1L, projectId, "presentation.pptx", Report.FileType.PPTX,
|
||||
tempDir.resolve("1/presentation.pptx").toString(), LocalDateTime.now());
|
||||
|
||||
when(reportRepository.save(any(Report.class))).thenReturn(savedReport);
|
||||
|
||||
// When
|
||||
ReportResponse response = reportService.uploadReport(file, projectId, fileType);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals("PPTX", response.getFileType());
|
||||
}
|
||||
|
||||
// Note: File type validation is done in Controller, not Service
|
||||
// Service accepts all file types passed from Controller
|
||||
|
||||
// ==================== getReportsByProject Tests ====================
|
||||
|
||||
@Test
|
||||
void getReportsByProject_shouldFilterCorrectly() {
|
||||
// Given
|
||||
Long projectId = 1L;
|
||||
Report report1 = new Report(1L, projectId, "report1.html", Report.FileType.HTML,
|
||||
"/path/report1.html", LocalDateTime.now());
|
||||
Report report2 = new Report(2L, projectId, "report2.md", Report.FileType.MD,
|
||||
"/path/report2.md", LocalDateTime.now());
|
||||
|
||||
when(reportRepository.findByProjectId(projectId)).thenReturn(Arrays.asList(report1, report2));
|
||||
|
||||
// When
|
||||
List<ReportResponse> result = reportService.getAllReports(projectId);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("report1.html", result.get(0).getFileName());
|
||||
assertEquals("report2.md", result.get(1).getFileName());
|
||||
verify(reportRepository, times(1)).findByProjectId(projectId);
|
||||
verify(reportRepository, never()).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getReportsByProject_shouldReturnEmptyListForNonExistentProject() {
|
||||
// Given
|
||||
Long projectId = 999L;
|
||||
when(reportRepository.findByProjectId(projectId)).thenReturn(Collections.emptyList());
|
||||
|
||||
// When
|
||||
List<ReportResponse> result = reportService.getAllReports(projectId);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllReports_shouldReturnAllReportsWhenProjectIdIsNull() {
|
||||
// Given
|
||||
Report report1 = new Report(1L, 1L, "report1.html", Report.FileType.HTML,
|
||||
"/path/report1.html", LocalDateTime.now());
|
||||
Report report2 = new Report(2L, 2L, "report2.md", Report.FileType.MD,
|
||||
"/path/report2.md", LocalDateTime.now());
|
||||
|
||||
when(reportRepository.findAll()).thenReturn(Arrays.asList(report1, report2));
|
||||
|
||||
// When
|
||||
List<ReportResponse> result = reportService.getAllReports(null);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
verify(reportRepository, times(1)).findAll();
|
||||
verify(reportRepository, never()).findByProjectId(any());
|
||||
}
|
||||
|
||||
// ==================== getReportById Tests ====================
|
||||
|
||||
@Test
|
||||
void getReportById_shouldReturnReportWithContent() throws Exception {
|
||||
// Given
|
||||
Long reportId = 1L;
|
||||
String filePath = tempDir.resolve("test_report.html").toString();
|
||||
Report report = new Report(reportId, 1L, "test_report.html", Report.FileType.HTML,
|
||||
filePath, LocalDateTime.now());
|
||||
|
||||
// Create the test file
|
||||
java.nio.file.Files.writeString(java.nio.file.Path.of(filePath), "<html>Content</html>");
|
||||
|
||||
when(reportRepository.findById(reportId)).thenReturn(Optional.of(report));
|
||||
|
||||
// When
|
||||
ReportResponse response = reportService.getReportById(reportId);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertEquals(reportId, response.getId());
|
||||
assertEquals("<html>Content</html>", response.getFileContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getReportById_shouldReturnNullWhenFileNotExists() {
|
||||
// Given
|
||||
Long reportId = 1L;
|
||||
String filePath = tempDir.resolve("non_existent.html").toString();
|
||||
Report report = new Report(reportId, 1L, "non_existent.html", Report.FileType.HTML,
|
||||
filePath, LocalDateTime.now());
|
||||
|
||||
when(reportRepository.findById(reportId)).thenReturn(Optional.of(report));
|
||||
|
||||
// When
|
||||
ReportResponse response = reportService.getReportById(reportId);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertNull(response.getFileContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getReportById_shouldThrowExceptionWhenNotFound() {
|
||||
// Given
|
||||
Long reportId = 999L;
|
||||
when(reportRepository.findById(reportId)).thenReturn(Optional.empty());
|
||||
|
||||
// When & Then
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
|
||||
reportService.getReportById(reportId);
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("Report not found"));
|
||||
}
|
||||
|
||||
// ==================== deleteReport Tests ====================
|
||||
|
||||
@Test
|
||||
void deleteReport_shouldDeleteSuccessfully() throws Exception {
|
||||
// Given
|
||||
Long reportId = 1L;
|
||||
String filePath = tempDir.resolve("delete_test.html").toString();
|
||||
Report report = new Report(reportId, 1L, "delete_test.html", Report.FileType.HTML,
|
||||
filePath, LocalDateTime.now());
|
||||
|
||||
// Create the test file
|
||||
java.nio.file.Files.writeString(java.nio.file.Path.of(filePath), "<html>To be deleted</html>");
|
||||
|
||||
when(reportRepository.findById(reportId)).thenReturn(Optional.of(report));
|
||||
doNothing().when(reportRepository).deleteById(reportId);
|
||||
|
||||
// When
|
||||
reportService.deleteReport(reportId);
|
||||
|
||||
// Then
|
||||
verify(reportRepository, times(1)).findById(reportId);
|
||||
verify(reportRepository, times(1)).deleteById(reportId);
|
||||
assertFalse(java.nio.file.Files.exists(java.nio.file.Path.of(filePath)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteReport_shouldHandleNonExistentFile() throws Exception {
|
||||
// Given
|
||||
Long reportId = 1L;
|
||||
String filePath = tempDir.resolve("non_existent_file.html").toString();
|
||||
Report report = new Report(reportId, 1L, "non_existent_file.html", Report.FileType.HTML,
|
||||
filePath, LocalDateTime.now());
|
||||
|
||||
when(reportRepository.findById(reportId)).thenReturn(Optional.of(report));
|
||||
doNothing().when(reportRepository).deleteById(reportId);
|
||||
|
||||
// When - Should not throw even if file doesn't exist
|
||||
assertDoesNotThrow(() -> reportService.deleteReport(reportId));
|
||||
|
||||
// Then
|
||||
verify(reportRepository, times(1)).findById(reportId);
|
||||
verify(reportRepository, times(1)).deleteById(reportId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteReport_shouldThrowExceptionWhenNotFound() {
|
||||
// Given
|
||||
Long reportId = 999L;
|
||||
when(reportRepository.findById(reportId)).thenReturn(Optional.empty());
|
||||
|
||||
// When & Then
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
|
||||
reportService.deleteReport(reportId);
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("Report not found"));
|
||||
verify(reportRepository, times(1)).findById(reportId);
|
||||
verify(reportRepository, never()).deleteById(any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user