From 1b6ef5eeed8c67489a088efd5e07fba43f84a328 Mon Sep 17 00:00:00 2001 From: Augment Agent Date: Fri, 1 May 2026 20:13:42 +0000 Subject: [PATCH] fix(delcus): cross-validate body CommCustno vs path, single-space CommDelFailCd Closes #23. * DelcusController: when the body's CommCustno is non-empty, it must match the path customerNumber (compared as long, tolerating leading zeros). Mismatch returns HTTP 400 with the standard 'Validation failed' ProblemDetail so a misaddressed request can never silently delete the path target. * DelcusController: emit ' ' instead of '' for CommDelFailCd on success to preserve the fixed-width 1-char commarea contract for clients porting from COBOL. * DelcusControllerWebMvcTest: tighten happy-path assertion and add rejectsBodyCommCustnoThatMismatchesPath + acceptsMatchingBodyCommCustno. --- .../cbsa/web/delcus/DelcusController.java | 21 ++++++- .../delcus/DelcusControllerWebMvcTest.java | 62 ++++++++++++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/augment/cbsa/web/delcus/DelcusController.java b/src/main/java/com/augment/cbsa/web/delcus/DelcusController.java index eed4d9a..c4cffe8 100644 --- a/src/main/java/com/augment/cbsa/web/delcus/DelcusController.java +++ b/src/main/java/com/augment/cbsa/web/delcus/DelcusController.java @@ -50,7 +50,21 @@ public ResponseEntity delete( // but DELCUS itself only consumes COMM-CUSTNO. Objects.requireNonNull(requestDto, "requestDto must not be null"); - DelcusResult result = delcusService.delete(new DelcusRequest(Long.parseLong(customerNumber))); + long pathCustomerNumber = Long.parseLong(customerNumber); + // Reject body/path mismatches up front. The body's CommCustno is + // optional (pattern allows ""); when supplied it must agree with the + // path so we never silently delete the path target on a misaddressed + // request. + String bodyCustno = requestDto.delCus().commCustno(); + if (bodyCustno != null && !bodyCustno.isEmpty() + && Long.parseLong(bodyCustno) != pathCustomerNumber) { + ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + problemDetail.setTitle("Validation failed"); + problemDetail.setDetail("Body CommCustno does not match path customerNumber."); + return ResponseEntity.badRequest().body(problemDetail); + } + + DelcusResult result = delcusService.delete(new DelcusRequest(pathCustomerNumber)); if (!result.deleteSuccess()) { return ResponseEntity.status(failureStatus(result)).body(failureBody(result)); } @@ -99,7 +113,10 @@ private DelcusResponseDto toResponse(DelcusResult result) { customer.creditScore(), toCobolDate(customer.csReviewDate()), "Y", - "" + // CommDelFailCd is a fixed-width 1-char commarea slot; + // emit a single space on success to preserve the length-1 + // contract for clients porting from COBOL. + " " )); } diff --git a/src/test/java/com/augment/cbsa/web/delcus/DelcusControllerWebMvcTest.java b/src/test/java/com/augment/cbsa/web/delcus/DelcusControllerWebMvcTest.java index 5691a8b..88c61e3 100644 --- a/src/test/java/com/augment/cbsa/web/delcus/DelcusControllerWebMvcTest.java +++ b/src/test/java/com/augment/cbsa/web/delcus/DelcusControllerWebMvcTest.java @@ -50,7 +50,67 @@ void returnsSuccessfulResponseForHappyPath() throws Exception { .andExpect(jsonPath("$.DelCus.CommEye").value("CUST")) .andExpect(jsonPath("$.DelCus.CommScode").value("987654")) .andExpect(jsonPath("$.DelCus.CommCustno").value("0000000001")) - .andExpect(jsonPath("$.DelCus.CommDelSuccess").value("Y")); + .andExpect(jsonPath("$.DelCus.CommDelSuccess").value("Y")) + .andExpect(jsonPath("$.DelCus.CommDelFailCd").value(" ")); + } + + @Test + void rejectsBodyCommCustnoThatMismatchesPath() throws Exception { + String body = """ + { + "DelCus": { + "CommEye": "CUST", + "CommScode": "987654", + "CommCustno": "0000000002", + "CommName": "", + "CommAddr": "", + "CommDob": 0, + "CommCreditScore": 0, + "CommCsReviewDate": 0, + "CommDelSuccess": " ", + "CommDelFailCd": " " + } + } + """; + + mockMvc.perform(delete("/api/v1/delcus/remove/1").contentType(APPLICATION_JSON).content(body)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.title").value("Validation failed")) + .andExpect(jsonPath("$.detail").value("Body CommCustno does not match path customerNumber.")); + } + + @Test + void acceptsMatchingBodyCommCustno() throws Exception { + when(delcusService.delete(new DelcusRequest(1L))).thenReturn(DelcusResult.success(new CustomerDetails( + "987654", + 1L, + "Mr Alice Example", + "1 Main Street", + LocalDate.of(2000, 1, 10), + 430, + LocalDate.of(2026, 5, 8) + ))); + + String body = """ + { + "DelCus": { + "CommEye": "CUST", + "CommScode": "987654", + "CommCustno": "0000000001", + "CommName": "", + "CommAddr": "", + "CommDob": 0, + "CommCreditScore": 0, + "CommCsReviewDate": 0, + "CommDelSuccess": " ", + "CommDelFailCd": " " + } + } + """; + + mockMvc.perform(delete("/api/v1/delcus/remove/1").contentType(APPLICATION_JSON).content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.DelCus.CommCustno").value("0000000001")); } @Test