Refactor report generation and enhance integration tests

- Updated ReportService to streamline Excel report merging logic by introducing a new method for conditional merging.
- Enhanced integration tests to include fiscal period handling and ensure correct artifact generation, particularly for scenarios with empty forecast sections.
- Improved test assertions to validate the presence of expected artifacts and their properties in the generated reports.
This commit is contained in:
DongHeon Jang 2026-04-17 11:58:10 +09:00
parent 661d78a225
commit a01aa46f3e
2 changed files with 95 additions and 10 deletions

View File

@ -201,11 +201,11 @@ public class ReportService {
Row titleRow = sheet.createRow(0); Row titleRow = sheet.createRow(0);
titleRow.setHeightInPoints(24); titleRow.setHeightInPoints(24);
createCell(titleRow, 0, sheetName + " Detail", styles.get("title")); createCell(titleRow, 0, sheetName + " Detail", styles.get("title"));
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, Math.max(0, columnCount - 1))); mergeIfNeeded(sheet, 0, columnCount);
Row subtitleRow = sheet.createRow(1); Row subtitleRow = sheet.createRow(1);
createCell(subtitleRow, 0, "Fiscal period " + fiscalPeriod + " | Rows " + rows.size(), styles.get("subtitle")); createCell(subtitleRow, 0, "Fiscal period " + fiscalPeriod + " | Rows " + rows.size(), styles.get("subtitle"));
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, Math.max(0, columnCount - 1))); mergeIfNeeded(sheet, 1, columnCount);
if (rows.isEmpty()) { if (rows.isEmpty()) {
Row emptyRow = sheet.createRow(3); Row emptyRow = sheet.createRow(3);
@ -347,6 +347,12 @@ public class ReportService {
cell.setCellStyle(style); cell.setCellStyle(style);
} }
private void mergeIfNeeded(Sheet sheet, int rowIndex, int columnCount) {
if (columnCount > 1) {
sheet.addMergedRegion(new CellRangeAddress(rowIndex, rowIndex, 0, columnCount - 1));
}
}
private CellStyle styleForKey(Map<String, CellStyle> styles, String key) { private CellStyle styleForKey(Map<String, CellStyle> styles, String key) {
return key.toLowerCase().contains("ratio") ? styles.get("ratio") : styles.get("number"); return key.toLowerCase().contains("ratio") ? styles.get("ratio") : styles.get("number");
} }

View File

@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.hanwha.nexacrodemo.upload.TestWorkbookFactory; import com.hanwha.nexacrodemo.upload.TestWorkbookFactory;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import org.apache.poi.ss.usermodel.WorkbookFactory; import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -20,12 +21,14 @@ import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockHttpSession;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc @AutoConfigureMockMvc
@ActiveProfiles("test") @ActiveProfiles("test")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class ConsolidationIntegrationTest { class ConsolidationIntegrationTest {
@Autowired @Autowired
@ -39,24 +42,27 @@ class ConsolidationIntegrationTest {
@Test @Test
void validUploadsCanBeConsolidatedAndReported() throws Exception { void validUploadsCanBeConsolidatedAndReported() throws Exception {
String fiscalPeriod = "2026-04";
MockHttpSession session = login("operator", "demo1234"); MockHttpSession session = login("operator", "demo1234");
upload(session, "trial-balance", "tb-valid.xlsx", TestWorkbookFactory.trialBalanceValid()); upload(session, "trial-balance", fiscalPeriod, "tb-valid.xlsx", withFiscalPeriod(TestWorkbookFactory.trialBalanceValid(), fiscalPeriod));
upload(session, "forecast", "forecast-valid.xlsx", TestWorkbookFactory.forecastValid()); upload(session, "forecast", fiscalPeriod, "forecast-valid.xlsx", withFiscalPeriod(TestWorkbookFactory.forecastValid(), fiscalPeriod));
MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs") MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.session(session) .session(session)
.content(""" .content("""
{ {
"fiscalPeriod": "2026-03", "fiscalPeriod": "%s",
"reportCurrency": "KRW" "reportCurrency": "KRW"
} }
""")) """.formatted(fiscalPeriod)))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.statusCode").value("REQUESTED")) .andExpect(jsonPath("$.statusCode").value("REQUESTED"))
.andReturn(); .andReturn();
long runId = objectMapper.readTree(runResult.getResponse().getContentAsByteArray()).path("id").asLong();
consolidationService.processPendingRuns("test-worker"); consolidationService.processPendingRuns("test-worker");
mockMvc.perform(get("/api/tx/consolidations/overview").session(session)) mockMvc.perform(get("/api/tx/consolidations/overview").session(session))
@ -65,20 +71,24 @@ class ConsolidationIntegrationTest {
MvcResult overviewResult = mockMvc.perform(get("/api/tx/reports/overview").session(session)) MvcResult overviewResult = mockMvc.perform(get("/api/tx/reports/overview").session(session))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.datasets.artifacts.length()").value(2))
.andExpect(jsonPath("$.datasets.jobLogs[0].logLevel").exists()) .andExpect(jsonPath("$.datasets.jobLogs[0].logLevel").exists())
.andReturn(); .andReturn();
JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray()) JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray())
.path("datasets") .path("datasets")
.path("artifacts"); .path("artifacts");
int artifactCountForRun = 0;
long excelArtifactId = -1L; long excelArtifactId = -1L;
for (JsonNode artifact : artifacts) { for (JsonNode artifact : artifacts) {
if (artifact.path("runId").asLong() != runId) {
continue;
}
artifactCountForRun++;
if ("EXCEL".equals(artifact.path("artifactType").asText())) { if ("EXCEL".equals(artifact.path("artifactType").asText())) {
excelArtifactId = artifact.path("id").asLong(); excelArtifactId = artifact.path("id").asLong();
break;
} }
} }
Assertions.assertEquals(2, artifactCountForRun);
Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist"); Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist");
MvcResult excelDownload = mockMvc.perform(get("/api/reports/{artifactId}/download", excelArtifactId).session(session)) MvcResult excelDownload = mockMvc.perform(get("/api/reports/{artifactId}/download", excelArtifactId).session(session))
@ -92,7 +102,61 @@ class ConsolidationIntegrationTest {
} }
} }
private void upload(MockHttpSession session, String templateCode, String fileName, byte[] content) throws Exception { @Test
void consolidationStillSucceedsWhenForecastSectionIsEmpty() throws Exception {
String fiscalPeriod = "2026-03";
MockHttpSession session = login("operator", "demo1234");
upload(session, "trial-balance", fiscalPeriod, "tb-valid.xlsx", TestWorkbookFactory.trialBalanceValid());
MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs")
.contentType(MediaType.APPLICATION_JSON)
.session(session)
.content("""
{
"fiscalPeriod": "%s",
"reportCurrency": "KRW"
}
""".formatted(fiscalPeriod)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.statusCode").value("REQUESTED"))
.andReturn();
long runId = objectMapper.readTree(runResult.getResponse().getContentAsByteArray()).path("id").asLong();
consolidationService.processPendingRuns("test-worker");
MvcResult overviewResult = mockMvc.perform(get("/api/tx/reports/overview").session(session))
.andExpect(status().isOk())
.andReturn();
JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray())
.path("datasets")
.path("artifacts");
int artifactCountForRun = 0;
long excelArtifactId = -1L;
for (JsonNode artifact : artifacts) {
if (artifact.path("runId").asLong() != runId) {
continue;
}
artifactCountForRun++;
if ("EXCEL".equals(artifact.path("artifactType").asText())) {
excelArtifactId = artifact.path("id").asLong();
}
}
Assertions.assertEquals(2, artifactCountForRun);
Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist");
MvcResult excelDownload = mockMvc.perform(get("/api/reports/{artifactId}/download", excelArtifactId).session(session))
.andExpect(status().isOk())
.andReturn();
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(excelDownload.getResponse().getContentAsByteArray()))) {
Assertions.assertEquals("No data available", workbook.getSheet("Forecast").getRow(3).getCell(0).getStringCellValue());
}
}
private void upload(MockHttpSession session, String templateCode, String fiscalPeriod, String fileName, byte[] content) throws Exception {
MockMultipartFile file = new MockMultipartFile( MockMultipartFile file = new MockMultipartFile(
"file", "file",
fileName, fileName,
@ -103,12 +167,27 @@ class ConsolidationIntegrationTest {
mockMvc.perform(multipart("/api/uploads") mockMvc.perform(multipart("/api/uploads")
.file(file) .file(file)
.param("templateCode", templateCode) .param("templateCode", templateCode)
.param("fiscalPeriod", "2026-03") .param("fiscalPeriod", fiscalPeriod)
.session(session)) .session(session))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.statusCode").value("ACCEPTED")); .andExpect(jsonPath("$.statusCode").value("ACCEPTED"));
} }
private byte[] withFiscalPeriod(byte[] content, String fiscalPeriod) throws Exception {
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(content))) {
var dataSheet = workbook.getSheet("DATA");
for (int rowIndex = 1; rowIndex <= dataSheet.getLastRowNum(); rowIndex++) {
var row = dataSheet.getRow(rowIndex);
if (row != null && row.getCell(0) != null) {
row.getCell(0).setCellValue(fiscalPeriod);
}
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private MockHttpSession login(String username, String password) throws Exception { private MockHttpSession login(String username, String password) throws Exception {
MvcResult result = mockMvc.perform(post("/api/auth/login") MvcResult result = mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)