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);
titleRow.setHeightInPoints(24);
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);
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()) {
Row emptyRow = sheet.createRow(3);
@ -347,6 +347,12 @@ public class ReportService {
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) {
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.hanwha.nexacrodemo.upload.TestWorkbookFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.junit.jupiter.api.Assertions;
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.MockMultipartFile;
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.MvcResult;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class ConsolidationIntegrationTest {
@Autowired
@ -39,24 +42,27 @@ class ConsolidationIntegrationTest {
@Test
void validUploadsCanBeConsolidatedAndReported() throws Exception {
String fiscalPeriod = "2026-04";
MockHttpSession session = login("operator", "demo1234");
upload(session, "trial-balance", "tb-valid.xlsx", TestWorkbookFactory.trialBalanceValid());
upload(session, "forecast", "forecast-valid.xlsx", TestWorkbookFactory.forecastValid());
upload(session, "trial-balance", fiscalPeriod, "tb-valid.xlsx", withFiscalPeriod(TestWorkbookFactory.trialBalanceValid(), fiscalPeriod));
upload(session, "forecast", fiscalPeriod, "forecast-valid.xlsx", withFiscalPeriod(TestWorkbookFactory.forecastValid(), fiscalPeriod));
MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs")
.contentType(MediaType.APPLICATION_JSON)
.session(session)
.content("""
{
"fiscalPeriod": "2026-03",
"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");
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))
.andExpect(status().isOk())
.andExpect(jsonPath("$.datasets.artifacts.length()").value(2))
.andExpect(jsonPath("$.datasets.jobLogs[0].logLevel").exists())
.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();
break;
}
}
Assertions.assertEquals(2, artifactCountForRun);
Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist");
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(
"file",
fileName,
@ -103,12 +167,27 @@ class ConsolidationIntegrationTest {
mockMvc.perform(multipart("/api/uploads")
.file(file)
.param("templateCode", templateCode)
.param("fiscalPeriod", "2026-03")
.param("fiscalPeriod", fiscalPeriod)
.session(session))
.andExpect(status().isOk())
.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 {
MvcResult result = mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)