diff --git a/src/main/java/com/augment/cbsa/web/delacc/DelaccController.java b/src/main/java/com/augment/cbsa/web/delacc/DelaccController.java index 77ce1ff..ee6832c 100644 --- a/src/main/java/com/augment/cbsa/web/delacc/DelaccController.java +++ b/src/main/java/com/augment/cbsa/web/delacc/DelaccController.java @@ -1,9 +1,11 @@ package com.augment.cbsa.web.delacc; +import com.augment.cbsa.config.CbsaProperties; import com.augment.cbsa.domain.AccountDetails; import com.augment.cbsa.domain.DelaccRequest; import com.augment.cbsa.domain.DelaccResult; import com.augment.cbsa.service.DelaccService; +import com.augment.cbsa.web.delacc.dto.DelaccCommareaRequestDto; import com.augment.cbsa.web.delacc.dto.DelaccCommareaResponseDto; import com.augment.cbsa.web.delacc.dto.DelaccRequestDto; import com.augment.cbsa.web.delacc.dto.DelaccResponseDto; @@ -33,9 +35,11 @@ public class DelaccController { private static final DateTimeFormatter COBOL_DATE_FORMATTER = DateTimeFormatter.ofPattern("ddMMyyyy", Locale.ROOT); private final DelaccService delaccService; + private final CbsaProperties cbsaProperties; - public DelaccController(DelaccService delaccService) { + public DelaccController(DelaccService delaccService, CbsaProperties cbsaProperties) { this.delaccService = Objects.requireNonNull(delaccService, "delaccService must not be null"); + this.cbsaProperties = Objects.requireNonNull(cbsaProperties, "cbsaProperties must not be null"); } @DeleteMapping("/remove/{accno}") @@ -48,6 +52,15 @@ public ResponseEntity delete( ) { Objects.requireNonNull(requestDto, "requestDto must not be null"); + // The body's DelAccAccno and DelAccScode are optional (patterns allow "" + // / null). When present they must agree with the path account number + // and the configured branch sortcode so a misaddressed request can + // never silently delete the path target. + ProblemDetail mismatch = validateBodyAgainstPath(requestDto.delAcc(), accountNumber); + if (mismatch != null) { + return ResponseEntity.badRequest().body(mismatch); + } + DelaccResult result = delaccService.delete(new DelaccRequest(accountNumber)); if (!result.deleteSuccess()) { return ResponseEntity.status(failureStatus(result)).body(failureBody(result)); @@ -56,6 +69,26 @@ public ResponseEntity delete( return ResponseEntity.ok(toResponse(result)); } + private ProblemDetail validateBodyAgainstPath(DelaccCommareaRequestDto delAcc, long pathAccountNumber) { + Long bodyAccno = delAcc.delAccAccno(); + if (bodyAccno != null && bodyAccno != pathAccountNumber) { + return mismatch("Body DelAccAccno does not match path accno."); + } + String bodyScode = delAcc.delAccScode(); + if (bodyScode != null && !bodyScode.isEmpty() + && !bodyScode.equals(cbsaProperties.sortcode())) { + return mismatch("Body DelAccScode does not match the configured branch sortcode."); + } + return null; + } + + private ProblemDetail mismatch(String detail) { + ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + problemDetail.setTitle("Validation failed"); + problemDetail.setDetail(detail); + return problemDetail; + } + private HttpStatus failureStatus(DelaccResult result) { if (result.isNotFoundFailure()) { return HttpStatus.NOT_FOUND; diff --git a/src/test/java/com/augment/cbsa/web/delacc/DelaccControllerWebMvcTest.java b/src/test/java/com/augment/cbsa/web/delacc/DelaccControllerWebMvcTest.java index a5ed105..5dd5214 100644 --- a/src/test/java/com/augment/cbsa/web/delacc/DelaccControllerWebMvcTest.java +++ b/src/test/java/com/augment/cbsa/web/delacc/DelaccControllerWebMvcTest.java @@ -10,9 +10,11 @@ import java.time.LocalDate; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.Matchers.containsString; @@ -26,6 +28,8 @@ @WebMvcTest(DelaccController.class) @Import(CbsaExceptionHandler.class) +@EnableConfigurationProperties(com.augment.cbsa.config.CbsaProperties.class) +@TestPropertySource(properties = "cbsa.sortcode=987654") class DelaccControllerWebMvcTest { @Autowired @@ -88,6 +92,47 @@ void requestValidationFailuresRemainProblemDetails() throws Exception { .andExpect(jsonPath("$.title").value("Validation failed")); } + @Test + void rejectsBodyAccnoThatMismatchesPath() throws Exception { + String body = requestJson().replace("\"DelAccAccno\": 12345678", "\"DelAccAccno\": 99999999"); + mockMvc.perform(delete("/api/v1/delacc/remove/12345678").contentType(APPLICATION_JSON).content(body)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.title").value("Validation failed")) + .andExpect(jsonPath("$.detail").value("Body DelAccAccno does not match path accno.")); + } + + @Test + void rejectsBodyScodeThatMismatchesConfiguredSortcode() throws Exception { + String body = requestJson().replace("\"DelAccScode\": \"987654\"", "\"DelAccScode\": \"123456\""); + mockMvc.perform(delete("/api/v1/delacc/remove/12345678").contentType(APPLICATION_JSON).content(body)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.title").value("Validation failed")) + .andExpect(jsonPath("$.detail").value("Body DelAccScode does not match the configured branch sortcode.")); + } + + @Test + void allowsEmptyOrNullBodyKeyFields() throws Exception { + when(delaccService.delete(new DelaccRequest(12345678L))).thenReturn(DelaccResult.success(new AccountDetails( + "987654", + 10L, + 12345678L, + "ISA", + new BigDecimal("1.50"), + LocalDate.of(2024, 1, 2), + new BigDecimal("250.00"), + LocalDate.of(2024, 2, 3), + LocalDate.of(2024, 3, 4), + new BigDecimal("1500.25"), + new BigDecimal("1499.75") + ))); + + String body = requestJson() + .replace("\"DelAccAccno\": 12345678,", "") + .replace("\"DelAccScode\": \"987654\"", "\"DelAccScode\": \"\""); + mockMvc.perform(delete("/api/v1/delacc/remove/12345678").contentType(APPLICATION_JSON).content(body)) + .andExpect(status().isOk()); + } + @Test void redactsAbendExceptionMessageFromResponseBody() throws Exception { when(delaccService.delete(new DelaccRequest(12345678L)))