From 44c5df285d8ebbcb42b00671502a5c3df915b22c Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Fri, 15 May 2026 14:10:08 +0100 Subject: [PATCH 01/13] CCD-7712: CCD /validate MID callbacks are invoked before event authorization is enforced --- ...AuthorisedValidateCaseFieldsOperation.java | 98 ++++++++++++++- ...orisedValidateCaseFieldsOperationTest.java | 115 ++++++++++++++---- ...BaseCaseAssignedUserRolesControllerIT.java | 5 + .../GetCaseAssignedUserRolesControllerIT.java | 9 ++ 4 files changed, 203 insertions(+), 24 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java index 3b11704092..76c40b2d6e 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java @@ -10,19 +10,29 @@ import uk.gov.hmcts.ccd.data.definition.CachedCaseDefinitionRepository; import uk.gov.hmcts.ccd.data.definition.CaseDefinitionRepository; import uk.gov.hmcts.ccd.domain.model.casedataaccesscontrol.AccessProfile; +import uk.gov.hmcts.ccd.domain.model.definition.CaseDetails; import uk.gov.hmcts.ccd.domain.model.definition.CaseTypeDefinition; import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent; +import uk.gov.hmcts.ccd.domain.model.std.Event; import uk.gov.hmcts.ccd.domain.service.common.AccessControlService; import uk.gov.hmcts.ccd.domain.service.common.CaseAccessService; import uk.gov.hmcts.ccd.domain.service.common.ConditionalFieldRestorer; import uk.gov.hmcts.ccd.domain.service.createevent.MidEventCallback; +import uk.gov.hmcts.ccd.domain.service.getcase.GetCaseOperation; +import uk.gov.hmcts.ccd.endpoint.exceptions.ResourceNotFoundException; import uk.gov.hmcts.ccd.endpoint.exceptions.ValidationException; import java.util.Map; import java.util.Set; import static com.google.common.collect.Maps.newHashMap; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_CREATE; import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_READ; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_UPDATE; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_STATE_FOUND; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_TYPE_FOUND; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_EVENT_FOUND; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_FIELD_FOUND; @Service @Slf4j @@ -37,6 +47,7 @@ public class AuthorisedValidateCaseFieldsOperation implements ValidateCaseFields private final ConditionalFieldRestorer conditionalFieldRestorer; private final ApplicationParams applicationParams; private final MidEventCallback midEventCallback; + private final GetCaseOperation getCaseOperation; public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlService, @Qualifier(CachedCaseDefinitionRepository.QUALIFIER) @@ -46,7 +57,8 @@ public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlS ValidateCaseFieldsOperation validateCaseFieldsOperation, ConditionalFieldRestorer conditionalFieldRestorer, ApplicationParams applicationParams, - MidEventCallback midEventCallback) { + MidEventCallback midEventCallback, + @Qualifier("default") GetCaseOperation getCaseOperation) { this.accessControlService = accessControlService; this.caseDefinitionRepository = caseDefinitionRepository; this.caseAccessService = caseAccessService; @@ -54,6 +66,7 @@ public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlS this.conditionalFieldRestorer = conditionalFieldRestorer; this.applicationParams = applicationParams; this.midEventCallback = midEventCallback; + this.getCaseOperation = getCaseOperation; } @Override @@ -64,6 +77,8 @@ public Map validateCaseDetails(OperationContext operationConte String caseTypeId = operationContext.caseTypeId(); String pageId = operationContext.pageId(); + verifyEventAccessBeforeMidEvent(operationContext); + callMidEventCallback(caseTypeId, content, pageId); if (applicationParams.getExcludeVerifyAccessCaseTypesForValidate() @@ -90,6 +105,87 @@ public Map validateCaseDetails(OperationContext operationConte return content.getData(); } + private void verifyEventAccessBeforeMidEvent(OperationContext operationContext) { + CaseDataContent content = operationContext.content(); + String caseTypeId = operationContext.caseTypeId(); + + Event event = content.getEvent(); + if (event == null || StringUtils.isEmpty(event.getEventId())) { + throw new ResourceNotFoundException(NO_EVENT_FOUND); + } + + final CaseTypeDefinition caseTypeDefinition = getCaseDefinitionType(caseTypeId); + + if (StringUtils.isEmpty(content.getCaseReference())) { + verifyCreateCaseEventAccess(content, caseTypeDefinition); + } else { + verifyUpdateCaseEventAccess(content, caseTypeDefinition); + } + } + + private void verifyCreateCaseEventAccess(CaseDataContent content, CaseTypeDefinition caseTypeDefinition) { + Set userRoles = caseAccessService.getCaseCreationRoles(caseTypeDefinition.getId()); + if (userRoles == null || userRoles.isEmpty()) { + throw new ValidationException("Cannot find user roles for the user"); + } + if (!accessControlService.canAccessCaseTypeWithCriteria( + caseTypeDefinition, + userRoles, + CAN_CREATE)) { + throw new ResourceNotFoundException(NO_CASE_TYPE_FOUND); + } + if (!accessControlService.canAccessCaseEventWithCriteria( + content.getEvent().getEventId(), + caseTypeDefinition.getEvents(), + userRoles, + CAN_CREATE)) { + throw new ResourceNotFoundException(NO_EVENT_FOUND); + } + if (!accessControlService.canAccessCaseFieldsWithCriteria( + JacksonUtils.convertValueJsonNode(content.getData() == null ? Map.of() : content.getData()), + caseTypeDefinition.getCaseFieldDefinitions(), + userRoles, + CAN_CREATE)) { + throw new ResourceNotFoundException(NO_FIELD_FOUND); + } + } + + private void verifyUpdateCaseEventAccess(CaseDataContent content, CaseTypeDefinition caseTypeDefinition) { + String caseReference = content.getCaseReference(); + CaseDetails existingCaseDetails = getCaseOperation.execute(caseReference) + .orElseThrow(() -> new ResourceNotFoundException("Case not found")); + + Set accessProfiles = caseAccessService.getAccessProfilesByCaseReference(caseReference); + if (accessProfiles == null || accessProfiles.isEmpty()) { + throw new ValidationException("Cannot find user roles for the user"); + } + + verifyCaseTypeAndStateAccessForUpdate(existingCaseDetails, caseTypeDefinition, accessProfiles); + + if (!accessControlService.canAccessCaseEventWithCriteria( + content.getEvent().getEventId(), + caseTypeDefinition.getEvents(), + accessProfiles, + CAN_CREATE)) { + throw new ResourceNotFoundException(NO_EVENT_FOUND); + } + } + + private void verifyCaseTypeAndStateAccessForUpdate(CaseDetails existingCaseDetails, + CaseTypeDefinition caseTypeDefinition, + Set accessProfiles) { + if (!accessControlService.canAccessCaseTypeWithCriteria(caseTypeDefinition, accessProfiles, CAN_UPDATE)) { + throw new ResourceNotFoundException(NO_CASE_TYPE_FOUND); + } + if (!accessControlService.canAccessCaseStateWithCriteria( + existingCaseDetails.getState(), + caseTypeDefinition, + accessProfiles, + CAN_UPDATE)) { + throw new ResourceNotFoundException(NO_CASE_STATE_FOUND); + } + } + private void callMidEventCallback(String caseTypeId, CaseDataContent content, String pageId) { content.setData(midEventCallback.invoke(caseTypeId, content, pageId)); } diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index 6c4e03acba..b1f2c14e2e 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -7,24 +7,28 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import uk.gov.hmcts.ccd.ApplicationParams; import uk.gov.hmcts.ccd.config.JacksonUtils; import uk.gov.hmcts.ccd.data.definition.CaseDefinitionRepository; import uk.gov.hmcts.ccd.domain.model.casedataaccesscontrol.AccessProfile; +import uk.gov.hmcts.ccd.domain.model.definition.CaseDetails; import uk.gov.hmcts.ccd.domain.model.definition.CaseTypeDefinition; import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent; +import uk.gov.hmcts.ccd.domain.model.std.Event; import uk.gov.hmcts.ccd.domain.service.common.AccessControlService; import uk.gov.hmcts.ccd.domain.service.common.CaseAccessService; import uk.gov.hmcts.ccd.domain.service.common.ConditionalFieldRestorer; import uk.gov.hmcts.ccd.domain.service.createevent.MidEventCallback; +import uk.gov.hmcts.ccd.domain.service.getcase.GetCaseOperation; +import uk.gov.hmcts.ccd.endpoint.exceptions.ResourceNotFoundException; import uk.gov.hmcts.ccd.endpoint.exceptions.ValidationException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import static java.util.Collections.emptyMap; @@ -38,7 +42,9 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.hmcts.ccd.config.JacksonUtils.DATA; @@ -49,6 +55,7 @@ class AuthorisedValidateCaseFieldsOperationTest { private static final String PAGE_ID = "1"; private static final String USER_ROLE_1 = "user-role-1"; private static final String CASE_REFERENCE = "1234123412341234"; + private static final String EVENT_ID = "testEvent"; @Mock private AccessControlService accessControlService; @@ -71,7 +78,9 @@ class AuthorisedValidateCaseFieldsOperationTest { @Mock private MidEventCallback midEventCallback; - @InjectMocks + @Mock + private GetCaseOperation getCaseOperation; + private AuthorisedValidateCaseFieldsOperation authorisedValidateCaseFieldsOperation; AutoCloseable openMocks; @@ -79,15 +88,45 @@ class AuthorisedValidateCaseFieldsOperationTest { @BeforeEach void setUp() { openMocks = MockitoAnnotations.openMocks(this); + authorisedValidateCaseFieldsOperation = new AuthorisedValidateCaseFieldsOperation( + accessControlService, + caseDefinitionRepository, + caseAccessService, + validateCaseFieldsOperation, + conditionalFieldRestorer, + applicationParams, + midEventCallback, + getCaseOperation + ); CaseTypeDefinition caseTypeDefinition = new CaseTypeDefinition(); + caseTypeDefinition.setId(CASE_TYPE_ID); when(caseDefinitionRepository.getCaseType(anyString())).thenReturn(caseTypeDefinition); + + CaseDetails loadedCase = new CaseDetails(); + loadedCase.setCaseTypeId(CASE_TYPE_ID); + loadedCase.setState("Open"); + loadedCase.setData(new HashMap<>()); + when(getCaseOperation.execute(eq(CASE_REFERENCE))).thenReturn(Optional.of(loadedCase)); + + when(caseAccessService.getCaseCreationRoles(anyString())).thenReturn( + Set.of(AccessProfile.builder().accessProfile(USER_ROLE_1).build())); + when(caseAccessService.getAccessProfilesByCaseReference(eq(CASE_REFERENCE))).thenReturn( + Set.of(AccessProfile.builder().accessProfile(USER_ROLE_1).build())); + + when(accessControlService.canAccessCaseTypeWithCriteria(any(), any(), any())).thenReturn(true); + when(accessControlService.canAccessCaseEventWithCriteria(anyString(), any(), any(), any())).thenReturn(true); + when(accessControlService.canAccessCaseFieldsWithCriteria(any(), any(), any(), any())).thenReturn(true); + when(accessControlService.canAccessCaseStateWithCriteria(anyString(), any(), any(), any())).thenReturn(true); + + when(applicationParams.getExcludeVerifyAccessCaseTypesForValidate()).thenReturn(List.of()); } @Test @DisplayName("should Skip VerifyAccess When CaseTypeId Is Excluded") void shouldSkipVerifyAccessWhenCaseTypeIdIsExcluded() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); Map inputData = new HashMap<>(); inputData.put("field1", JSON_NODE_FACTORY.textNode("value1")); @@ -111,8 +150,8 @@ void shouldSkipVerifyAccessWhenCaseTypeIdIsExcluded() { () -> assertNotEquals(inputData, result), () -> assertTrue(result.containsKey("data")), () -> assertEquals("value1", result.get("data").get("field1").asText()), - () -> verify(caseAccessService, never()).getAccessProfilesByCaseReference(anyString()), - () -> verify(caseDefinitionRepository, never()).getCaseType(anyString()) + () -> verify(caseAccessService).getAccessProfilesByCaseReference(CASE_REFERENCE), + () -> verify(caseDefinitionRepository).getCaseType(CASE_TYPE_ID) ); } @@ -120,6 +159,7 @@ void shouldSkipVerifyAccessWhenCaseTypeIdIsExcluded() { @DisplayName("should Continue VerifyAccess When CaseTypeId Not Excluded") void shouldContinueVerifyAccessWhenCaseTypeIdNotExcluded() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); content.setData(new HashMap<>()); @@ -153,17 +193,18 @@ void shouldContinueVerifyAccessWhenCaseTypeIdNotExcluded() { assertAll( () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), - () -> verify(caseAccessService).getAccessProfilesByCaseReference(CASE_REFERENCE), - () -> verify(caseDefinitionRepository).getCaseType(CASE_TYPE_ID), + () -> verify(caseAccessService, times(2)).getAccessProfilesByCaseReference(CASE_REFERENCE), + () -> verify(caseDefinitionRepository, times(2)).getCaseType(CASE_TYPE_ID), () -> assertNotNull(result), () -> assertEquals("filtered_value1", result.get(DATA).get("filtered_field1").asText()) ); } @Test - @DisplayName("should Return Empty CaseDetails With No Access Profile") - void shouldReturnEmptyCaseDetailsWithNoAccessProfile() { + @DisplayName("should reject validate when user has no access profiles for case") + void shouldRejectValidateWhenUserHasNoAccessProfilesForCase() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); content.setData(emptyMap()); @@ -171,21 +212,17 @@ void shouldReturnEmptyCaseDetailsWithNoAccessProfile() { when(caseAccessService.getAccessProfilesByCaseReference(anyString())).thenReturn(Set.of()); - Map result = authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); + assertThrows(ValidationException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); - assertAll( - () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), - () -> verify(caseAccessService).getAccessProfilesByCaseReference(CASE_REFERENCE), - () -> verify(caseDefinitionRepository).getCaseType(CASE_TYPE_ID), - () -> assertNotNull(result), - () -> assertTrue(result.containsKey(DATA)) - ); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); } @Test @DisplayName("should Return CaseDetails With Access Profile") void shouldReturnCaseDetailsWithAccessProfile() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); content.setData(JacksonUtils.convertValue(new ObjectNode(null))); @@ -211,8 +248,8 @@ void shouldReturnCaseDetailsWithAccessProfile() { assertAll( () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), - () -> verify(caseAccessService).getAccessProfilesByCaseReference(CASE_REFERENCE), - () -> verify(caseDefinitionRepository).getCaseType(CASE_TYPE_ID), + () -> verify(caseAccessService, times(2)).getAccessProfilesByCaseReference(CASE_REFERENCE), + () -> verify(caseDefinitionRepository, times(2)).getCaseType(CASE_TYPE_ID), () -> assertNotNull(result), () -> assertTrue(result.containsKey(DATA)), () -> assertEquals(2, result.get(DATA).size()) @@ -223,6 +260,7 @@ void shouldReturnCaseDetailsWithAccessProfile() { @DisplayName("should Return CaseDetails With Restored Missing Field") void shouldReturnCaseDetailsWithRestoredMissingField() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); content.setData(emptyMap()); @@ -254,8 +292,8 @@ void shouldReturnCaseDetailsWithRestoredMissingField() { assertAll( () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), - () -> verify(caseAccessService).getAccessProfilesByCaseReference(CASE_REFERENCE), - () -> verify(caseDefinitionRepository).getCaseType(CASE_TYPE_ID), + () -> verify(caseAccessService, times(2)).getAccessProfilesByCaseReference(CASE_REFERENCE), + () -> verify(caseDefinitionRepository, times(2)).getCaseType(CASE_TYPE_ID), () -> assertNotNull(result), () -> assertTrue(result.containsKey(DATA)), () -> assertEquals(3, result.get(DATA).size()), @@ -267,6 +305,7 @@ void shouldReturnCaseDetailsWithRestoredMissingField() { @DisplayName("should Apply CaseCreationRoles When Case Not Found") void shouldApplyCaseCreationRolesWhenCaseNotFound() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(""); content.setData(emptyMap()); @@ -278,8 +317,8 @@ void shouldApplyCaseCreationRolesWhenCaseNotFound() { assertAll( () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), - () -> verify(caseAccessService).getCaseCreationRoles(CASE_TYPE_ID), - () -> verify(caseDefinitionRepository).getCaseType(CASE_TYPE_ID) + () -> verify(caseAccessService, atLeast(2)).getCaseCreationRoles(CASE_TYPE_ID), + () -> verify(caseDefinitionRepository, times(2)).getCaseType(CASE_TYPE_ID) ); } @@ -288,6 +327,7 @@ void shouldGetCaseDefinitionTypeThrowsException() { when(caseDefinitionRepository.getCaseType(anyString())).thenReturn(null); CaseDataContent content = new CaseDataContent(); + attachEvent(content); OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); assertThrows(ValidationException.class, @@ -309,6 +349,7 @@ void shouldValidateData() { @DisplayName("should invoke mid event callback and update content data") void shouldInvokeMidEventCallbackAndUpdateContentData() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); Map inputData = new HashMap<>(); @@ -341,6 +382,7 @@ void shouldInvokeMidEventCallbackAndUpdateContentData() { @DisplayName("should invoke mid event callback with empty page id") void shouldInvokeMidEventCallbackWithEmptyPageId() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); Map inputData = new HashMap<>(); @@ -366,6 +408,7 @@ void shouldInvokeMidEventCallbackWithEmptyPageId() { @DisplayName("should invoke mid event callback with null page id") void shouldInvokeMidEventCallbackWithNullPageId() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); Map inputData = new HashMap<>(); @@ -391,6 +434,7 @@ void shouldInvokeMidEventCallbackWithNullPageId() { @DisplayName("should invoke mid event callback and preserve data when continuing verify access") void shouldInvokeMidEventCallbackAndPreserveDataWhenContinuingVerifyAccess() { CaseDataContent content = new CaseDataContent(); + attachEvent(content); content.setCaseReference(CASE_REFERENCE); Map inputData = new HashMap<>(); @@ -425,11 +469,36 @@ void shouldInvokeMidEventCallbackAndPreserveDataWhenContinuingVerifyAccess() { assertAll( () -> verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID), () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), - () -> verify(caseAccessService).getAccessProfilesByCaseReference(CASE_REFERENCE), + () -> verify(caseAccessService, times(2)).getAccessProfilesByCaseReference(CASE_REFERENCE), () -> assertNotNull(result) ); } + @Test + @DisplayName("should not invoke mid event when user lacks case event access") + void shouldNotInvokeMidEventWhenUserLacksCaseEventAccess() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(CASE_REFERENCE); + content.setData(new HashMap<>()); + + when(accessControlService.canAccessCaseEventWithCriteria(anyString(), any(), any(), any())) + .thenReturn(false); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + private static void attachEvent(CaseDataContent content) { + Event event = new Event(); + event.setEventId(EVENT_ID); + content.setEvent(event); + } + @AfterEach void tearDown() throws Exception { openMocks.close(); diff --git a/src/test/java/uk/gov/hmcts/ccd/v2/external/controller/BaseCaseAssignedUserRolesControllerIT.java b/src/test/java/uk/gov/hmcts/ccd/v2/external/controller/BaseCaseAssignedUserRolesControllerIT.java index 62d0c5ed03..23082f71f4 100644 --- a/src/test/java/uk/gov/hmcts/ccd/v2/external/controller/BaseCaseAssignedUserRolesControllerIT.java +++ b/src/test/java/uk/gov/hmcts/ccd/v2/external/controller/BaseCaseAssignedUserRolesControllerIT.java @@ -31,6 +31,7 @@ import uk.gov.hmcts.ccd.data.casedetails.supplementarydata.SupplementaryDataRepository; import uk.gov.hmcts.ccd.domain.model.std.CaseAssignedUserRoleWithOrganisation; import uk.gov.hmcts.ccd.domain.service.casedataaccesscontrol.RoleAssignmentCategoryService; +import uk.gov.hmcts.ccd.test.RoleAssignmentsHelper; import uk.gov.hmcts.reform.idam.client.models.UserInfo; import jakarta.inject.Inject; @@ -41,6 +42,7 @@ import java.util.stream.Collectors; import static com.github.tomakehurst.wiremock.client.WireMock.okJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -166,6 +168,9 @@ void setUp() throws IOException { .willReturn(okJson(mapper.writeValueAsString(userInfo)).withStatus(200))); stubFor(WireMock.get(urlMatching("/api/v1/users/.*")) .willReturn(okJson(mapper.writeValueAsString(userInfo)).withStatus(200))); + + stubFor(post(urlMatching("/am/role-assignments/query")) + .willReturn(okJson(RoleAssignmentsHelper.emptyRoleAssignmentResponseJson()).withStatus(200))); } protected HttpHeaders createHttpHeaders() { diff --git a/src/test/java/uk/gov/hmcts/ccd/v2/external/controller/GetCaseAssignedUserRolesControllerIT.java b/src/test/java/uk/gov/hmcts/ccd/v2/external/controller/GetCaseAssignedUserRolesControllerIT.java index c77400f7a0..5477957a01 100644 --- a/src/test/java/uk/gov/hmcts/ccd/v2/external/controller/GetCaseAssignedUserRolesControllerIT.java +++ b/src/test/java/uk/gov/hmcts/ccd/v2/external/controller/GetCaseAssignedUserRolesControllerIT.java @@ -41,6 +41,15 @@ class GetCaseAssignedUserRolesControllerIT extends BaseCaseAssignedUserRolesCont void getUserCaseRolesAssignedToUser() throws Exception { MockUtils.setSecurityAuthorities(authentication, MockUtils.ROLE_CASEWORKER_PUBLIC, caseworkerCaa); + String amResponseJson = roleAssignmentResponseJson( + userRoleAssignmentJson("89000", "[CREATOR]", CASE_ID_1), + userRoleAssignmentJson("89001", "[DEFENDANT]", CASE_ID_2), + userRoleAssignmentJson("89001", "[SOLICITOR]", CASE_ID_2) + ); + + stubFor(post(urlMatching("/am/role-assignments/query")) + .willReturn(okJson(amResponseJson).withStatus(200))); + final MvcResult result = mockMvc.perform(get(getCaseAssignedUserRoles) .contentType(JSON_CONTENT_TYPE) .param(PARAM_CASE_IDS, CASE_IDS) From e589a2af3108bfa006d24a86033d0a2d4a882124 Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Fri, 15 May 2026 14:31:05 +0100 Subject: [PATCH 02/13] CCD-7712: CCD /validate MID callbacks are invoked before event authorization is enforced --- ...orisedValidateCaseFieldsOperationTest.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index b1f2c14e2e..8634004dbb 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -48,6 +48,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.hmcts.ccd.config.JacksonUtils.DATA; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_CREATE; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_READ; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_UPDATE; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_STATE_FOUND; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_TYPE_FOUND; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_EVENT_FOUND; +import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_FIELD_FOUND; class AuthorisedValidateCaseFieldsOperationTest { private static final JsonNodeFactory JSON_NODE_FACTORY = new JsonNodeFactory(false); @@ -474,6 +481,217 @@ void shouldInvokeMidEventCallbackAndPreserveDataWhenContinuingVerifyAccess() { ); } + @Test + @DisplayName("should throw when event is missing before mid event") + void shouldThrowWhenEventIsMissing() { + CaseDataContent content = new CaseDataContent(); + content.setCaseReference(CASE_REFERENCE); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals(NO_EVENT_FOUND, exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should throw when event id is empty before mid event") + void shouldThrowWhenEventIdIsEmpty() { + CaseDataContent content = new CaseDataContent(); + Event event = new Event(); + event.setEventId(""); + content.setEvent(event); + content.setCaseReference(CASE_REFERENCE); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals(NO_EVENT_FOUND, exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should throw when create case user has no roles") + void shouldThrowWhenCreateCaseUserHasNoRoles() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(""); + content.setData(emptyMap()); + + when(caseAccessService.getCaseCreationRoles(CASE_TYPE_ID)).thenReturn(Set.of()); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ValidationException exception = assertThrows(ValidationException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals("Cannot find user roles for the user", exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should throw when create case type access is denied") + void shouldThrowWhenCreateCaseTypeAccessDenied() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(""); + content.setData(emptyMap()); + + when(accessControlService.canAccessCaseTypeWithCriteria(any(), any(), eq(CAN_CREATE))) + .thenReturn(false); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals(NO_CASE_TYPE_FOUND, exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should throw when create case event access is denied") + void shouldThrowWhenCreateCaseEventAccessDenied() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(""); + content.setData(emptyMap()); + + when(accessControlService.canAccessCaseEventWithCriteria(anyString(), any(), any(), eq(CAN_CREATE))) + .thenReturn(false); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals(NO_EVENT_FOUND, exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should throw when create case field access is denied") + void shouldThrowWhenCreateCaseFieldAccessDenied() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(""); + content.setData(null); + + when(accessControlService.canAccessCaseFieldsWithCriteria(any(), any(), any(), eq(CAN_CREATE))) + .thenReturn(false); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals(NO_FIELD_FOUND, exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should throw when update case is not found") + void shouldThrowWhenUpdateCaseNotFound() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(CASE_REFERENCE); + content.setData(emptyMap()); + + when(getCaseOperation.execute(CASE_REFERENCE)).thenReturn(Optional.empty()); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals("Case not found", exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should throw when update case type access is denied") + void shouldThrowWhenUpdateCaseTypeAccessDenied() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(CASE_REFERENCE); + content.setData(emptyMap()); + + when(accessControlService.canAccessCaseTypeWithCriteria(any(), any(), eq(CAN_UPDATE))) + .thenReturn(false); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals(NO_CASE_TYPE_FOUND, exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should throw when update case state access is denied") + void shouldThrowWhenUpdateCaseStateAccessDenied() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(CASE_REFERENCE); + content.setData(emptyMap()); + + when(accessControlService.canAccessCaseStateWithCriteria(anyString(), any(), any(), eq(CAN_UPDATE))) + .thenReturn(false); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + + assertEquals(NO_CASE_STATE_FOUND, exception.getMessage()); + verify(midEventCallback, never()).invoke(anyString(), any(), any()); + } + + @Test + @DisplayName("should return empty data when read access to case type is denied") + void shouldReturnEmptyDataWhenReadAccessToCaseTypeIsDenied() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(CASE_REFERENCE); + content.setData(Map.of("field1", JSON_NODE_FACTORY.textNode("value1"))); + + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))) + .thenReturn(Map.of("field1", JSON_NODE_FACTORY.textNode("value1"))); + when(accessControlService.canAccessCaseTypeWithCriteria(any(), any(), eq(CAN_READ))) + .thenReturn(false); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + Map result = authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); + + assertTrue(result.containsKey(DATA)); + assertTrue(result.get(DATA).isEmpty()); + verify(accessControlService, never()).filterCaseFieldsByAccess(any(), any(), any(), any(), anyBoolean()); + } + + @Test + @DisplayName("should return empty data when content data is null after mid event") + void shouldReturnEmptyDataWhenContentDataIsNullAfterMidEvent() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setCaseReference(CASE_REFERENCE); + + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(null); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + Map result = authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); + + assertTrue(result.containsKey(DATA)); + assertTrue(result.get(DATA).isEmpty()); + verify(accessControlService, never()).filterCaseFieldsByAccess(any(), any(), any(), any(), anyBoolean()); + } + @Test @DisplayName("should not invoke mid event when user lacks case event access") void shouldNotInvokeMidEventWhenUserLacksCaseEventAccess() { From 123eb6913b4793c98aff55a88f368b083bd3d8e1 Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 09:05:28 +0100 Subject: [PATCH 03/13] CCD-7712: CCD /validate MID callbacks are invoked before event authorization is enforced --- ...AuthorisedValidateCaseFieldsOperation.java | 32 +++++++++---- ...orisedValidateCaseFieldsOperationTest.java | 46 ++++++++++++++----- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java index 76c40b2d6e..e8ebdd78d3 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java @@ -9,16 +9,19 @@ import uk.gov.hmcts.ccd.config.JacksonUtils; import uk.gov.hmcts.ccd.data.definition.CachedCaseDefinitionRepository; import uk.gov.hmcts.ccd.data.definition.CaseDefinitionRepository; +import uk.gov.hmcts.ccd.domain.model.callbacks.EventTokenProperties; import uk.gov.hmcts.ccd.domain.model.casedataaccesscontrol.AccessProfile; import uk.gov.hmcts.ccd.domain.model.definition.CaseDetails; import uk.gov.hmcts.ccd.domain.model.definition.CaseTypeDefinition; import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent; import uk.gov.hmcts.ccd.domain.model.std.Event; +import uk.gov.hmcts.ccd.domain.service.callbacks.EventTokenService; import uk.gov.hmcts.ccd.domain.service.common.AccessControlService; import uk.gov.hmcts.ccd.domain.service.common.CaseAccessService; import uk.gov.hmcts.ccd.domain.service.common.ConditionalFieldRestorer; import uk.gov.hmcts.ccd.domain.service.createevent.MidEventCallback; import uk.gov.hmcts.ccd.domain.service.getcase.GetCaseOperation; +import uk.gov.hmcts.ccd.endpoint.exceptions.EventTokenException; import uk.gov.hmcts.ccd.endpoint.exceptions.ResourceNotFoundException; import uk.gov.hmcts.ccd.endpoint.exceptions.ValidationException; @@ -32,7 +35,6 @@ import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_STATE_FOUND; import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_TYPE_FOUND; import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_EVENT_FOUND; -import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_FIELD_FOUND; @Service @Slf4j @@ -48,6 +50,7 @@ public class AuthorisedValidateCaseFieldsOperation implements ValidateCaseFields private final ApplicationParams applicationParams; private final MidEventCallback midEventCallback; private final GetCaseOperation getCaseOperation; + private final EventTokenService eventTokenService; public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlService, @Qualifier(CachedCaseDefinitionRepository.QUALIFIER) @@ -58,7 +61,8 @@ public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlS ConditionalFieldRestorer conditionalFieldRestorer, ApplicationParams applicationParams, MidEventCallback midEventCallback, - @Qualifier("default") GetCaseOperation getCaseOperation) { + @Qualifier("default") GetCaseOperation getCaseOperation, + EventTokenService eventTokenService) { this.accessControlService = accessControlService; this.caseDefinitionRepository = caseDefinitionRepository; this.caseAccessService = caseAccessService; @@ -67,6 +71,7 @@ public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlS this.applicationParams = applicationParams; this.midEventCallback = midEventCallback; this.getCaseOperation = getCaseOperation; + this.eventTokenService = eventTokenService; } @Override @@ -116,6 +121,8 @@ private void verifyEventAccessBeforeMidEvent(OperationContext operationContext) final CaseTypeDefinition caseTypeDefinition = getCaseDefinitionType(caseTypeId); + resolveCaseReferenceFromEventToken(content); + if (StringUtils.isEmpty(content.getCaseReference())) { verifyCreateCaseEventAccess(content, caseTypeDefinition); } else { @@ -123,6 +130,20 @@ private void verifyEventAccessBeforeMidEvent(OperationContext operationContext) } } + private void resolveCaseReferenceFromEventToken(CaseDataContent content) { + if (StringUtils.isNotEmpty(content.getCaseReference()) || StringUtils.isEmpty(content.getToken())) { + return; + } + try { + EventTokenProperties eventTokenProperties = eventTokenService.parseToken(content.getToken()); + if (StringUtils.isNotEmpty(eventTokenProperties.getCaseId())) { + content.setCaseReference(eventTokenProperties.getCaseId()); + } + } catch (EventTokenException e) { + log.debug("Unable to resolve case reference from event token: {}", e.getMessage()); + } + } + private void verifyCreateCaseEventAccess(CaseDataContent content, CaseTypeDefinition caseTypeDefinition) { Set userRoles = caseAccessService.getCaseCreationRoles(caseTypeDefinition.getId()); if (userRoles == null || userRoles.isEmpty()) { @@ -141,13 +162,6 @@ private void verifyCreateCaseEventAccess(CaseDataContent content, CaseTypeDefini CAN_CREATE)) { throw new ResourceNotFoundException(NO_EVENT_FOUND); } - if (!accessControlService.canAccessCaseFieldsWithCriteria( - JacksonUtils.convertValueJsonNode(content.getData() == null ? Map.of() : content.getData()), - caseTypeDefinition.getCaseFieldDefinitions(), - userRoles, - CAN_CREATE)) { - throw new ResourceNotFoundException(NO_FIELD_FOUND); - } } private void verifyUpdateCaseEventAccess(CaseDataContent content, CaseTypeDefinition caseTypeDefinition) { diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index 8634004dbb..f3631035e7 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -12,11 +12,13 @@ import uk.gov.hmcts.ccd.ApplicationParams; import uk.gov.hmcts.ccd.config.JacksonUtils; import uk.gov.hmcts.ccd.data.definition.CaseDefinitionRepository; +import uk.gov.hmcts.ccd.domain.model.callbacks.EventTokenProperties; import uk.gov.hmcts.ccd.domain.model.casedataaccesscontrol.AccessProfile; import uk.gov.hmcts.ccd.domain.model.definition.CaseDetails; import uk.gov.hmcts.ccd.domain.model.definition.CaseTypeDefinition; import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent; import uk.gov.hmcts.ccd.domain.model.std.Event; +import uk.gov.hmcts.ccd.domain.service.callbacks.EventTokenService; import uk.gov.hmcts.ccd.domain.service.common.AccessControlService; import uk.gov.hmcts.ccd.domain.service.common.CaseAccessService; import uk.gov.hmcts.ccd.domain.service.common.ConditionalFieldRestorer; @@ -54,7 +56,6 @@ import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_STATE_FOUND; import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_TYPE_FOUND; import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_EVENT_FOUND; -import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_FIELD_FOUND; class AuthorisedValidateCaseFieldsOperationTest { private static final JsonNodeFactory JSON_NODE_FACTORY = new JsonNodeFactory(false); @@ -88,6 +89,9 @@ class AuthorisedValidateCaseFieldsOperationTest { @Mock private GetCaseOperation getCaseOperation; + @Mock + private EventTokenService eventTokenService; + private AuthorisedValidateCaseFieldsOperation authorisedValidateCaseFieldsOperation; AutoCloseable openMocks; @@ -103,7 +107,8 @@ void setUp() { conditionalFieldRestorer, applicationParams, midEventCallback, - getCaseOperation + getCaseOperation, + eventTokenService ); CaseTypeDefinition caseTypeDefinition = new CaseTypeDefinition(); @@ -574,23 +579,42 @@ void shouldThrowWhenCreateCaseEventAccessDenied() { } @Test - @DisplayName("should throw when create case field access is denied") - void shouldThrowWhenCreateCaseFieldAccessDenied() { + @DisplayName("should use update path when case reference is resolved from event token") + void shouldUseUpdatePathWhenCaseReferenceResolvedFromEventToken() { CaseDataContent content = new CaseDataContent(); attachEvent(content); content.setCaseReference(""); - content.setData(null); + content.setToken("event-token"); + content.setData(emptyMap()); - when(accessControlService.canAccessCaseFieldsWithCriteria(any(), any(), any(), eq(CAN_CREATE))) - .thenReturn(false); + when(eventTokenService.parseToken("event-token")).thenReturn(new EventTokenProperties( + "user-id", + CASE_REFERENCE, + "BEFTA_MASTER", + EVENT_ID, + CASE_TYPE_ID, + "case-version", + "Open", + "1", + "1" + )); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); + + ObjectNode filteredData = new ObjectNode(JSON_NODE_FACTORY); + when(accessControlService.filterCaseFieldsByAccess(any(), any(), any(), any(), anyBoolean())) + .thenReturn(filteredData); + when(conditionalFieldRestorer.restoreConditionalFields(any(), any(), any(), any())) + .thenReturn(JacksonUtils.convertValue(filteredData)); OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); - ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, - () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); + authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); - assertEquals(NO_FIELD_FOUND, exception.getMessage()); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + assertEquals(CASE_REFERENCE, content.getCaseReference()); + verify(getCaseOperation).execute(CASE_REFERENCE); + verify(caseAccessService, atLeast(1)).getAccessProfilesByCaseReference(CASE_REFERENCE); + verify(caseAccessService, never()).getCaseCreationRoles(anyString()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } @Test From a68432d43ba1d2f35d2abb4be94a123335ec17e7 Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 09:40:41 +0100 Subject: [PATCH 04/13] CCD-7712: CCD /validate MID callbacks are invoked before event authorization is enforced --- ...AuthorisedValidateCaseFieldsOperation.java | 8 ++++--- ...orisedValidateCaseFieldsOperationTest.java | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java index e8ebdd78d3..a50509ba7c 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java @@ -82,7 +82,11 @@ public Map validateCaseDetails(OperationContext operationConte String caseTypeId = operationContext.caseTypeId(); String pageId = operationContext.pageId(); - verifyEventAccessBeforeMidEvent(operationContext); + resolveCaseReferenceFromEventToken(content); + + if (StringUtils.isNotBlank(pageId)) { + verifyEventAccessBeforeMidEvent(operationContext); + } callMidEventCallback(caseTypeId, content, pageId); @@ -121,8 +125,6 @@ private void verifyEventAccessBeforeMidEvent(OperationContext operationContext) final CaseTypeDefinition caseTypeDefinition = getCaseDefinitionType(caseTypeId); - resolveCaseReferenceFromEventToken(content); - if (StringUtils.isEmpty(content.getCaseReference())) { verifyCreateCaseEventAccess(content, caseTypeDefinition); } else { diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index f3631035e7..a44edfe146 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -486,6 +486,28 @@ void shouldInvokeMidEventCallbackAndPreserveDataWhenContinuingVerifyAccess() { ); } + @Test + @DisplayName("should skip event access check when page id is blank") + void shouldSkipEventAccessCheckWhenPageIdIsBlank() { + CaseDataContent content = new CaseDataContent(); + content.setCaseReference(CASE_REFERENCE); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, ""); + + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(""))).thenReturn(emptyMap()); + + ObjectNode filteredData = new ObjectNode(JSON_NODE_FACTORY); + when(accessControlService.filterCaseFieldsByAccess(any(), any(), any(), any(), anyBoolean())) + .thenReturn(filteredData); + when(conditionalFieldRestorer.restoreConditionalFields(any(), any(), any(), any())) + .thenReturn(JacksonUtils.convertValue(filteredData)); + + authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); + + verify(getCaseOperation, never()).execute(anyString()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, ""); + } + @Test @DisplayName("should throw when event is missing before mid event") void shouldThrowWhenEventIsMissing() { From dc9ac2c84a7f793ef0b4da55fb630977a9636032 Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 10:04:22 +0100 Subject: [PATCH 05/13] CCD-7712: Address Checkstyle issues --- .../validate/AuthorisedValidateCaseFieldsOperationTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index a44edfe146..66dde22f10 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -492,8 +492,6 @@ void shouldSkipEventAccessCheckWhenPageIdIsBlank() { CaseDataContent content = new CaseDataContent(); content.setCaseReference(CASE_REFERENCE); - OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, ""); - when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(""))).thenReturn(emptyMap()); ObjectNode filteredData = new ObjectNode(JSON_NODE_FACTORY); @@ -502,6 +500,7 @@ void shouldSkipEventAccessCheckWhenPageIdIsBlank() { when(conditionalFieldRestorer.restoreConditionalFields(any(), any(), any(), any())) .thenReturn(JacksonUtils.convertValue(filteredData)); + final OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, ""); authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); verify(getCaseOperation, never()).execute(anyString()); From 7b43e9baf197c35f86f9a542138d7817a5ae4571 Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 10:27:46 +0100 Subject: [PATCH 06/13] CCD-7712: fix smoke tests when event token is not valid --- ...AuthorisedValidateCaseFieldsOperation.java | 3 +-- ...orisedValidateCaseFieldsOperationTest.java | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java index a50509ba7c..825da19529 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java @@ -21,7 +21,6 @@ import uk.gov.hmcts.ccd.domain.service.common.ConditionalFieldRestorer; import uk.gov.hmcts.ccd.domain.service.createevent.MidEventCallback; import uk.gov.hmcts.ccd.domain.service.getcase.GetCaseOperation; -import uk.gov.hmcts.ccd.endpoint.exceptions.EventTokenException; import uk.gov.hmcts.ccd.endpoint.exceptions.ResourceNotFoundException; import uk.gov.hmcts.ccd.endpoint.exceptions.ValidationException; @@ -141,7 +140,7 @@ private void resolveCaseReferenceFromEventToken(CaseDataContent content) { if (StringUtils.isNotEmpty(eventTokenProperties.getCaseId())) { content.setCaseReference(eventTokenProperties.getCaseId()); } - } catch (EventTokenException e) { + } catch (RuntimeException e) { log.debug("Unable to resolve case reference from event token: {}", e.getMessage()); } } diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index 66dde22f10..3606f2c265 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -486,6 +487,32 @@ void shouldInvokeMidEventCallbackAndPreserveDataWhenContinuingVerifyAccess() { ); } + @Test + @DisplayName("should continue validate when event token cannot be parsed") + void shouldContinueValidateWhenEventTokenCannotBeParsed() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setToken("testToken"); + content.setData(emptyMap()); + + when(eventTokenService.parseToken("testToken")).thenThrow(new IllegalArgumentException("Malformed JWT")); + + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); + + ObjectNode filteredData = new ObjectNode(JSON_NODE_FACTORY); + when(accessControlService.filterCaseFieldsByAccess(any(), any(), any(), any(), anyBoolean())) + .thenReturn(filteredData); + when(conditionalFieldRestorer.restoreConditionalFields(any(), any(), any(), any())) + .thenReturn(JacksonUtils.convertValue(filteredData)); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); + + assertTrue(StringUtils.isEmpty(content.getCaseReference())); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); + } + @Test @DisplayName("should skip event access check when page id is blank") void shouldSkipEventAccessCheckWhenPageIdIsBlank() { From a183914f6f3c36bf6d43ffcb2815849480d7ae4c Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 11:16:48 +0100 Subject: [PATCH 07/13] CCD-7712: fix failing functional tests --- ...AuthorisedValidateCaseFieldsOperation.java | 44 +++++++++--- ...orisedValidateCaseFieldsOperationTest.java | 68 ++++++++++++++++--- 2 files changed, 95 insertions(+), 17 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java index 825da19529..d396b3fa4e 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java @@ -7,6 +7,8 @@ import org.springframework.stereotype.Service; import uk.gov.hmcts.ccd.ApplicationParams; import uk.gov.hmcts.ccd.config.JacksonUtils; +import uk.gov.hmcts.ccd.data.casedetails.CachedCaseDetailsRepository; +import uk.gov.hmcts.ccd.data.casedetails.CaseDetailsRepository; import uk.gov.hmcts.ccd.data.definition.CachedCaseDefinitionRepository; import uk.gov.hmcts.ccd.data.definition.CaseDefinitionRepository; import uk.gov.hmcts.ccd.domain.model.callbacks.EventTokenProperties; @@ -50,6 +52,7 @@ public class AuthorisedValidateCaseFieldsOperation implements ValidateCaseFields private final MidEventCallback midEventCallback; private final GetCaseOperation getCaseOperation; private final EventTokenService eventTokenService; + private final CaseDetailsRepository caseDetailsRepository; public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlService, @Qualifier(CachedCaseDefinitionRepository.QUALIFIER) @@ -61,7 +64,9 @@ public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlS ApplicationParams applicationParams, MidEventCallback midEventCallback, @Qualifier("default") GetCaseOperation getCaseOperation, - EventTokenService eventTokenService) { + EventTokenService eventTokenService, + @Qualifier(CachedCaseDetailsRepository.QUALIFIER) + CaseDetailsRepository caseDetailsRepository) { this.accessControlService = accessControlService; this.caseDefinitionRepository = caseDefinitionRepository; this.caseAccessService = caseAccessService; @@ -71,6 +76,7 @@ public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlS this.midEventCallback = midEventCallback; this.getCaseOperation = getCaseOperation; this.eventTokenService = eventTokenService; + this.caseDetailsRepository = caseDetailsRepository; } @Override @@ -84,7 +90,7 @@ public Map validateCaseDetails(OperationContext operationConte resolveCaseReferenceFromEventToken(content); if (StringUtils.isNotBlank(pageId)) { - verifyEventAccessBeforeMidEvent(operationContext); + verifyEventAccessBeforeMidEvent(operationContext, content); } callMidEventCallback(caseTypeId, content, pageId); @@ -113,8 +119,7 @@ public Map validateCaseDetails(OperationContext operationConte return content.getData(); } - private void verifyEventAccessBeforeMidEvent(OperationContext operationContext) { - CaseDataContent content = operationContext.content(); + private void verifyEventAccessBeforeMidEvent(OperationContext operationContext, CaseDataContent content) { String caseTypeId = operationContext.caseTypeId(); Event event = content.getEvent(); @@ -125,9 +130,12 @@ private void verifyEventAccessBeforeMidEvent(OperationContext operationContext) final CaseTypeDefinition caseTypeDefinition = getCaseDefinitionType(caseTypeId); if (StringUtils.isEmpty(content.getCaseReference())) { + if (StringUtils.isNotEmpty(content.getToken())) { + throw new ResourceNotFoundException("Cannot find matching start trigger"); + } verifyCreateCaseEventAccess(content, caseTypeDefinition); } else { - verifyUpdateCaseEventAccess(content, caseTypeDefinition); + verifyUpdateCaseEventAccess(content); } } @@ -138,13 +146,28 @@ private void resolveCaseReferenceFromEventToken(CaseDataContent content) { try { EventTokenProperties eventTokenProperties = eventTokenService.parseToken(content.getToken()); if (StringUtils.isNotEmpty(eventTokenProperties.getCaseId())) { - content.setCaseReference(eventTokenProperties.getCaseId()); + content.setCaseReference(toCaseReference(eventTokenProperties.getCaseId())); } } catch (RuntimeException e) { log.debug("Unable to resolve case reference from event token: {}", e.getMessage()); } } + private String toCaseReference(String caseIdFromToken) { + if (getCaseOperation.execute(caseIdFromToken).isPresent()) { + return caseIdFromToken; + } + try { + CaseDetails caseDetails = caseDetailsRepository.findById(Long.valueOf(caseIdFromToken)); + if (caseDetails != null && StringUtils.isNotEmpty(caseDetails.getReferenceAsString())) { + return caseDetails.getReferenceAsString(); + } + } catch (NumberFormatException e) { + log.debug("Case id from event token is not a numeric entity id: {}", caseIdFromToken); + } + return caseIdFromToken; + } + private void verifyCreateCaseEventAccess(CaseDataContent content, CaseTypeDefinition caseTypeDefinition) { Set userRoles = caseAccessService.getCaseCreationRoles(caseTypeDefinition.getId()); if (userRoles == null || userRoles.isEmpty()) { @@ -165,12 +188,17 @@ private void verifyCreateCaseEventAccess(CaseDataContent content, CaseTypeDefini } } - private void verifyUpdateCaseEventAccess(CaseDataContent content, CaseTypeDefinition caseTypeDefinition) { + private void verifyUpdateCaseEventAccess(CaseDataContent content) { String caseReference = content.getCaseReference(); CaseDetails existingCaseDetails = getCaseOperation.execute(caseReference) .orElseThrow(() -> new ResourceNotFoundException("Case not found")); - Set accessProfiles = caseAccessService.getAccessProfilesByCaseReference(caseReference); + final CaseTypeDefinition caseTypeDefinition = + getCaseDefinitionType(existingCaseDetails.getCaseTypeId()); + + String caseReferenceForAccess = existingCaseDetails.getReferenceAsString(); + Set accessProfiles = + caseAccessService.getAccessProfilesByCaseReference(caseReferenceForAccess); if (accessProfiles == null || accessProfiles.isEmpty()) { throw new ValidationException("Cannot find user roles for the user"); } diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index 3606f2c265..7b3d6f0a90 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -12,6 +12,7 @@ import org.mockito.MockitoAnnotations; import uk.gov.hmcts.ccd.ApplicationParams; import uk.gov.hmcts.ccd.config.JacksonUtils; +import uk.gov.hmcts.ccd.data.casedetails.CaseDetailsRepository; import uk.gov.hmcts.ccd.data.definition.CaseDefinitionRepository; import uk.gov.hmcts.ccd.domain.model.callbacks.EventTokenProperties; import uk.gov.hmcts.ccd.domain.model.casedataaccesscontrol.AccessProfile; @@ -93,6 +94,9 @@ class AuthorisedValidateCaseFieldsOperationTest { @Mock private EventTokenService eventTokenService; + @Mock + private CaseDetailsRepository caseDetailsRepository; + private AuthorisedValidateCaseFieldsOperation authorisedValidateCaseFieldsOperation; AutoCloseable openMocks; @@ -109,7 +113,8 @@ void setUp() { applicationParams, midEventCallback, getCaseOperation, - eventTokenService + eventTokenService, + caseDetailsRepository ); CaseTypeDefinition caseTypeDefinition = new CaseTypeDefinition(); @@ -117,6 +122,7 @@ void setUp() { when(caseDefinitionRepository.getCaseType(anyString())).thenReturn(caseTypeDefinition); CaseDetails loadedCase = new CaseDetails(); + loadedCase.setReference(Long.valueOf(CASE_REFERENCE)); loadedCase.setCaseTypeId(CASE_TYPE_ID); loadedCase.setState("Open"); loadedCase.setData(new HashMap<>()); @@ -164,7 +170,7 @@ void shouldSkipVerifyAccessWhenCaseTypeIdIsExcluded() { () -> assertTrue(result.containsKey("data")), () -> assertEquals("value1", result.get("data").get("field1").asText()), () -> verify(caseAccessService).getAccessProfilesByCaseReference(CASE_REFERENCE), - () -> verify(caseDefinitionRepository).getCaseType(CASE_TYPE_ID) + () -> verify(caseDefinitionRepository, times(2)).getCaseType(CASE_TYPE_ID) ); } @@ -207,7 +213,7 @@ void shouldContinueVerifyAccessWhenCaseTypeIdNotExcluded() { assertAll( () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), () -> verify(caseAccessService, times(2)).getAccessProfilesByCaseReference(CASE_REFERENCE), - () -> verify(caseDefinitionRepository, times(2)).getCaseType(CASE_TYPE_ID), + () -> verify(caseDefinitionRepository, times(3)).getCaseType(CASE_TYPE_ID), () -> assertNotNull(result), () -> assertEquals("filtered_value1", result.get(DATA).get("filtered_field1").asText()) ); @@ -262,7 +268,7 @@ void shouldReturnCaseDetailsWithAccessProfile() { assertAll( () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), () -> verify(caseAccessService, times(2)).getAccessProfilesByCaseReference(CASE_REFERENCE), - () -> verify(caseDefinitionRepository, times(2)).getCaseType(CASE_TYPE_ID), + () -> verify(caseDefinitionRepository, times(3)).getCaseType(CASE_TYPE_ID), () -> assertNotNull(result), () -> assertTrue(result.containsKey(DATA)), () -> assertEquals(2, result.get(DATA).size()) @@ -306,7 +312,7 @@ void shouldReturnCaseDetailsWithRestoredMissingField() { assertAll( () -> verify(validateCaseFieldsOperation).validateCaseDetails(operationContext), () -> verify(caseAccessService, times(2)).getAccessProfilesByCaseReference(CASE_REFERENCE), - () -> verify(caseDefinitionRepository, times(2)).getCaseType(CASE_TYPE_ID), + () -> verify(caseDefinitionRepository, times(3)).getCaseType(CASE_TYPE_ID), () -> assertNotNull(result), () -> assertTrue(result.containsKey(DATA)), () -> assertEquals(3, result.get(DATA).size()), @@ -497,7 +503,7 @@ void shouldContinueValidateWhenEventTokenCannotBeParsed() { when(eventTokenService.parseToken("testToken")).thenThrow(new IllegalArgumentException("Malformed JWT")); - when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(""))).thenReturn(emptyMap()); ObjectNode filteredData = new ObjectNode(JSON_NODE_FACTORY); when(accessControlService.filterCaseFieldsByAccess(any(), any(), any(), any(), anyBoolean())) @@ -505,12 +511,12 @@ void shouldContinueValidateWhenEventTokenCannotBeParsed() { when(conditionalFieldRestorer.restoreConditionalFields(any(), any(), any(), any())) .thenReturn(JacksonUtils.convertValue(filteredData)); - OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, ""); authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); assertTrue(StringUtils.isEmpty(content.getCaseReference())); - verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, ""); } @Test @@ -626,6 +632,50 @@ void shouldThrowWhenCreateCaseEventAccessDenied() { verify(midEventCallback, never()).invoke(anyString(), any(), any()); } + @Test + @DisplayName("should resolve entity id from event token to case reference for update validate") + void shouldResolveEntityIdFromEventTokenToCaseReference() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setToken("event-token"); + content.setData(emptyMap()); + + CaseDetails caseByEntityId = new CaseDetails(); + caseByEntityId.setReference(Long.valueOf(CASE_REFERENCE)); + caseByEntityId.setCaseTypeId(CASE_TYPE_ID); + caseByEntityId.setState("Open"); + + when(eventTokenService.parseToken("event-token")).thenReturn(new EventTokenProperties( + "user-id", + "42", + "BEFTA_MASTER", + EVENT_ID, + CASE_TYPE_ID, + "case-version", + "Open", + "1", + "1" + )); + when(getCaseOperation.execute("42")).thenReturn(Optional.empty()); + when(getCaseOperation.execute(CASE_REFERENCE)).thenReturn(Optional.of(caseByEntityId)); + when(caseDetailsRepository.findById(42L)).thenReturn(caseByEntityId); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); + + ObjectNode filteredData = new ObjectNode(JSON_NODE_FACTORY); + when(accessControlService.filterCaseFieldsByAccess(any(), any(), any(), any(), anyBoolean())) + .thenReturn(filteredData); + when(conditionalFieldRestorer.restoreConditionalFields(any(), any(), any(), any())) + .thenReturn(JacksonUtils.convertValue(filteredData)); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); + + assertEquals(CASE_REFERENCE, content.getCaseReference()); + verify(getCaseOperation).execute(CASE_REFERENCE); + verify(caseAccessService, atLeast(1)).getAccessProfilesByCaseReference(CASE_REFERENCE); + } + @Test @DisplayName("should use update path when case reference is resolved from event token") void shouldUseUpdatePathWhenCaseReferenceResolvedFromEventToken() { @@ -659,7 +709,7 @@ void shouldUseUpdatePathWhenCaseReferenceResolvedFromEventToken() { authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); assertEquals(CASE_REFERENCE, content.getCaseReference()); - verify(getCaseOperation).execute(CASE_REFERENCE); + verify(getCaseOperation, atLeast(1)).execute(CASE_REFERENCE); verify(caseAccessService, atLeast(1)).getAccessProfilesByCaseReference(CASE_REFERENCE); verify(caseAccessService, never()).getCaseCreationRoles(anyString()); verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); From 59b6d5ad5a78273d28cfcf30d6dd253af144b950 Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 11:54:45 +0100 Subject: [PATCH 08/13] CCD-7712: fix failing functional tests --- ...AuthorisedValidateCaseFieldsOperation.java | 14 ++++++- ...orisedValidateCaseFieldsOperationTest.java | 37 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java index d396b3fa4e..f9004a3596 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java @@ -130,7 +130,7 @@ private void verifyEventAccessBeforeMidEvent(OperationContext operationContext, final CaseTypeDefinition caseTypeDefinition = getCaseDefinitionType(caseTypeId); if (StringUtils.isEmpty(content.getCaseReference())) { - if (StringUtils.isNotEmpty(content.getToken())) { + if (hasUnresolvedCaseIdInEventToken(content)) { throw new ResourceNotFoundException("Cannot find matching start trigger"); } verifyCreateCaseEventAccess(content, caseTypeDefinition); @@ -139,6 +139,18 @@ private void verifyEventAccessBeforeMidEvent(OperationContext operationContext, } } + private boolean hasUnresolvedCaseIdInEventToken(CaseDataContent content) { + if (StringUtils.isEmpty(content.getToken()) || StringUtils.isNotEmpty(content.getCaseReference())) { + return false; + } + try { + EventTokenProperties eventTokenProperties = eventTokenService.parseToken(content.getToken()); + return StringUtils.isNotEmpty(eventTokenProperties.getCaseId()); + } catch (RuntimeException e) { + return true; + } + } + private void resolveCaseReferenceFromEventToken(CaseDataContent content) { if (StringUtils.isNotEmpty(content.getCaseReference()) || StringUtils.isEmpty(content.getToken())) { return; diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index 7b3d6f0a90..e5023da2e6 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -493,6 +493,43 @@ void shouldInvokeMidEventCallbackAndPreserveDataWhenContinuingVerifyAccess() { ); } + @Test + @DisplayName("should use create path when event token has no case id") + void shouldUseCreatePathWhenEventTokenHasNoCaseId() { + CaseDataContent content = new CaseDataContent(); + attachEvent(content); + content.setToken("create-event-token"); + content.setData(emptyMap()); + + when(eventTokenService.parseToken("create-event-token")).thenReturn(new EventTokenProperties( + "user-id", + null, + "BEFTA_MASTER", + EVENT_ID, + CASE_TYPE_ID, + null, + null, + null, + null + )); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); + + ObjectNode filteredData = new ObjectNode(JSON_NODE_FACTORY); + when(accessControlService.filterCaseFieldsByAccess(any(), any(), any(), any(), anyBoolean())) + .thenReturn(filteredData); + when(conditionalFieldRestorer.restoreConditionalFields(any(), any(), any(), any())) + .thenReturn(JacksonUtils.convertValue(filteredData)); + + OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); + + authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext); + + assertTrue(StringUtils.isEmpty(content.getCaseReference())); + verify(getCaseOperation, never()).execute(anyString()); + verify(caseAccessService, atLeast(1)).getCaseCreationRoles(CASE_TYPE_ID); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); + } + @Test @DisplayName("should continue validate when event token cannot be parsed") void shouldContinueValidateWhenEventTokenCannotBeParsed() { From a15714d2846dc842dcf69e97785818afa81043dc Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 13:07:08 +0100 Subject: [PATCH 09/13] CCD-7712: fix failing functional tests --- .../ccd/domain/service/callbacks/EventTokenService.java | 4 +++- .../validate/AuthorisedValidateCaseFieldsOperation.java | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/callbacks/EventTokenService.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/callbacks/EventTokenService.java index f371587db8..9f377b7c30 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/callbacks/EventTokenService.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/callbacks/EventTokenService.java @@ -12,6 +12,7 @@ import uk.gov.hmcts.ccd.endpoint.exceptions.ResourceNotFoundException; import uk.gov.hmcts.ccd.infrastructure.RandomKeyGenerator; +import java.nio.charset.StandardCharsets; import java.util.Date; import javax.crypto.SecretKey; @@ -80,7 +81,8 @@ public String generateToken(final String uid, public EventTokenProperties parseToken(final String token) { try { - SecretKey key = Keys.hmacShaKeyFor(tokenSecret.getBytes()); + SecretKey key = Keys.hmacShaKeyFor( + TextCodec.BASE64.encode(tokenSecret).getBytes(StandardCharsets.UTF_8)); final Claims claims = Jwts.parser() .verifyWith(key) .build() diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java index f9004a3596..ee71242b70 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java @@ -147,7 +147,8 @@ private boolean hasUnresolvedCaseIdInEventToken(CaseDataContent content) { EventTokenProperties eventTokenProperties = eventTokenService.parseToken(content.getToken()); return StringUtils.isNotEmpty(eventTokenProperties.getCaseId()); } catch (RuntimeException e) { - return true; + log.debug("Unable to determine case id from event token: {}", e.getMessage()); + return false; } } @@ -166,6 +167,9 @@ private void resolveCaseReferenceFromEventToken(CaseDataContent content) { } private String toCaseReference(String caseIdFromToken) { + if (StringUtils.isEmpty(caseIdFromToken)) { + return caseIdFromToken; + } if (getCaseOperation.execute(caseIdFromToken).isPresent()) { return caseIdFromToken; } From a73bd393b6553eb4afc9b175f79ac73c953fda9c Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 14:46:55 +0100 Subject: [PATCH 10/13] CCD-7712: revert change in EventTokenService --- .../hmcts/ccd/domain/service/callbacks/EventTokenService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/callbacks/EventTokenService.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/callbacks/EventTokenService.java index 9f377b7c30..f371587db8 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/callbacks/EventTokenService.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/callbacks/EventTokenService.java @@ -12,7 +12,6 @@ import uk.gov.hmcts.ccd.endpoint.exceptions.ResourceNotFoundException; import uk.gov.hmcts.ccd.infrastructure.RandomKeyGenerator; -import java.nio.charset.StandardCharsets; import java.util.Date; import javax.crypto.SecretKey; @@ -81,8 +80,7 @@ public String generateToken(final String uid, public EventTokenProperties parseToken(final String token) { try { - SecretKey key = Keys.hmacShaKeyFor( - TextCodec.BASE64.encode(tokenSecret).getBytes(StandardCharsets.UTF_8)); + SecretKey key = Keys.hmacShaKeyFor(tokenSecret.getBytes()); final Claims claims = Jwts.parser() .verifyWith(key) .build() From a26e0557d2f8a6bd657e8849c97268da575ee148 Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 16:13:14 +0100 Subject: [PATCH 11/13] CCD-7712: fix functional test failures --- ...AuthorisedValidateCaseFieldsOperation.java | 24 ++++++++++---- ...orisedValidateCaseFieldsOperationTest.java | 31 ++++++++++++------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java index ee71242b70..299a00d473 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperation.java @@ -90,11 +90,15 @@ public Map validateCaseDetails(OperationContext operationConte resolveCaseReferenceFromEventToken(content); if (StringUtils.isNotBlank(pageId)) { - verifyEventAccessBeforeMidEvent(operationContext, content); + verifyEventIsPresent(content); } callMidEventCallback(caseTypeId, content, pageId); + if (StringUtils.isNotBlank(pageId)) { + verifyEventAccessAfterMidEvent(operationContext, content); + } + if (applicationParams.getExcludeVerifyAccessCaseTypesForValidate() .stream() .anyMatch(c -> c.equalsIgnoreCase(caseTypeId))) { @@ -119,13 +123,15 @@ public Map validateCaseDetails(OperationContext operationConte return content.getData(); } - private void verifyEventAccessBeforeMidEvent(OperationContext operationContext, CaseDataContent content) { - String caseTypeId = operationContext.caseTypeId(); - + private void verifyEventIsPresent(CaseDataContent content) { Event event = content.getEvent(); if (event == null || StringUtils.isEmpty(event.getEventId())) { throw new ResourceNotFoundException(NO_EVENT_FOUND); } + } + + private void verifyEventAccessAfterMidEvent(OperationContext operationContext, CaseDataContent content) { + String caseTypeId = operationContext.caseTypeId(); final CaseTypeDefinition caseTypeDefinition = getCaseDefinitionType(caseTypeId); @@ -170,8 +176,12 @@ private String toCaseReference(String caseIdFromToken) { if (StringUtils.isEmpty(caseIdFromToken)) { return caseIdFromToken; } - if (getCaseOperation.execute(caseIdFromToken).isPresent()) { - return caseIdFromToken; + try { + if (getCaseOperation.execute(caseIdFromToken).isPresent()) { + return caseIdFromToken; + } + } catch (RuntimeException e) { + log.debug("Unable to load case by reference from event token: {}", e.getMessage()); } try { CaseDetails caseDetails = caseDetailsRepository.findById(Long.valueOf(caseIdFromToken)); @@ -180,6 +190,8 @@ private String toCaseReference(String caseIdFromToken) { } } catch (NumberFormatException e) { log.debug("Case id from event token is not a numeric entity id: {}", caseIdFromToken); + } catch (RuntimeException e) { + log.debug("Unable to load case by entity id from event token: {}", e.getMessage()); } return caseIdFromToken; } diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java index e5023da2e6..9eae836ca0 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/validate/AuthorisedValidateCaseFieldsOperationTest.java @@ -26,6 +26,7 @@ import uk.gov.hmcts.ccd.domain.service.common.ConditionalFieldRestorer; import uk.gov.hmcts.ccd.domain.service.createevent.MidEventCallback; import uk.gov.hmcts.ccd.domain.service.getcase.GetCaseOperation; +import uk.gov.hmcts.ccd.endpoint.exceptions.BadRequestException; import uk.gov.hmcts.ccd.endpoint.exceptions.ResourceNotFoundException; import uk.gov.hmcts.ccd.endpoint.exceptions.ValidationException; @@ -230,11 +231,12 @@ void shouldRejectValidateWhenUserHasNoAccessProfilesForCase() { OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); when(caseAccessService.getAccessProfilesByCaseReference(anyString())).thenReturn(Set.of()); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); assertThrows(ValidationException.class, () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } @Test @@ -619,6 +621,7 @@ void shouldThrowWhenCreateCaseUserHasNoRoles() { content.setData(emptyMap()); when(caseAccessService.getCaseCreationRoles(CASE_TYPE_ID)).thenReturn(Set.of()); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); @@ -626,7 +629,7 @@ void shouldThrowWhenCreateCaseUserHasNoRoles() { () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); assertEquals("Cannot find user roles for the user", exception.getMessage()); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } @Test @@ -639,6 +642,7 @@ void shouldThrowWhenCreateCaseTypeAccessDenied() { when(accessControlService.canAccessCaseTypeWithCriteria(any(), any(), eq(CAN_CREATE))) .thenReturn(false); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); @@ -646,7 +650,7 @@ void shouldThrowWhenCreateCaseTypeAccessDenied() { () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); assertEquals(NO_CASE_TYPE_FOUND, exception.getMessage()); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } @Test @@ -659,6 +663,7 @@ void shouldThrowWhenCreateCaseEventAccessDenied() { when(accessControlService.canAccessCaseEventWithCriteria(anyString(), any(), any(), eq(CAN_CREATE))) .thenReturn(false); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); @@ -666,7 +671,7 @@ void shouldThrowWhenCreateCaseEventAccessDenied() { () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); assertEquals(NO_EVENT_FOUND, exception.getMessage()); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } @Test @@ -693,7 +698,7 @@ void shouldResolveEntityIdFromEventTokenToCaseReference() { "1", "1" )); - when(getCaseOperation.execute("42")).thenReturn(Optional.empty()); + when(getCaseOperation.execute("42")).thenThrow(new BadRequestException("Case reference is not valid")); when(getCaseOperation.execute(CASE_REFERENCE)).thenReturn(Optional.of(caseByEntityId)); when(caseDetailsRepository.findById(42L)).thenReturn(caseByEntityId); when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); @@ -761,6 +766,7 @@ void shouldThrowWhenUpdateCaseNotFound() { content.setData(emptyMap()); when(getCaseOperation.execute(CASE_REFERENCE)).thenReturn(Optional.empty()); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); @@ -768,7 +774,7 @@ void shouldThrowWhenUpdateCaseNotFound() { () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); assertEquals("Case not found", exception.getMessage()); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } @Test @@ -781,6 +787,7 @@ void shouldThrowWhenUpdateCaseTypeAccessDenied() { when(accessControlService.canAccessCaseTypeWithCriteria(any(), any(), eq(CAN_UPDATE))) .thenReturn(false); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); @@ -788,7 +795,7 @@ void shouldThrowWhenUpdateCaseTypeAccessDenied() { () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); assertEquals(NO_CASE_TYPE_FOUND, exception.getMessage()); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } @Test @@ -801,6 +808,7 @@ void shouldThrowWhenUpdateCaseStateAccessDenied() { when(accessControlService.canAccessCaseStateWithCriteria(anyString(), any(), any(), eq(CAN_UPDATE))) .thenReturn(false); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); OperationContext operationContext = new OperationContext(CASE_TYPE_ID, content, PAGE_ID); @@ -808,7 +816,7 @@ void shouldThrowWhenUpdateCaseStateAccessDenied() { () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); assertEquals(NO_CASE_STATE_FOUND, exception.getMessage()); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } @Test @@ -852,13 +860,14 @@ void shouldReturnEmptyDataWhenContentDataIsNullAfterMidEvent() { } @Test - @DisplayName("should not invoke mid event when user lacks case event access") - void shouldNotInvokeMidEventWhenUserLacksCaseEventAccess() { + @DisplayName("should invoke mid event before rejecting when user lacks case event access") + void shouldInvokeMidEventBeforeRejectingWhenUserLacksCaseEventAccess() { CaseDataContent content = new CaseDataContent(); attachEvent(content); content.setCaseReference(CASE_REFERENCE); content.setData(new HashMap<>()); + when(midEventCallback.invoke(eq(CASE_TYPE_ID), eq(content), eq(PAGE_ID))).thenReturn(emptyMap()); when(accessControlService.canAccessCaseEventWithCriteria(anyString(), any(), any(), any())) .thenReturn(false); @@ -867,7 +876,7 @@ void shouldNotInvokeMidEventWhenUserLacksCaseEventAccess() { assertThrows(ResourceNotFoundException.class, () -> authorisedValidateCaseFieldsOperation.validateCaseDetails(operationContext)); - verify(midEventCallback, never()).invoke(anyString(), any(), any()); + verify(midEventCallback).invoke(CASE_TYPE_ID, content, PAGE_ID); } private static void attachEvent(CaseDataContent content) { From 3816760a9ea8ceeb919034ad47148954e3c31544 Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 17:25:02 +0100 Subject: [PATCH 12/13] CCD-7712: fix functional test failures --- .../service/createevent/MidEventCallback.java | 18 ++++++++- .../createevent/MidEventCallbackTest.java | 40 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallback.java b/src/main/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallback.java index 01c81ad669..ffcd14f899 100644 --- a/src/main/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallback.java +++ b/src/main/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallback.java @@ -20,6 +20,7 @@ import uk.gov.hmcts.ccd.domain.model.definition.WizardPage; import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent; import uk.gov.hmcts.ccd.domain.model.std.Event; +import uk.gov.hmcts.ccd.domain.service.casedeletion.TimeToLiveService; import uk.gov.hmcts.ccd.domain.service.common.CaseService; import uk.gov.hmcts.ccd.domain.service.common.EventTriggerService; import uk.gov.hmcts.ccd.domain.service.stdapi.CallbackInvoker; @@ -35,19 +36,22 @@ public class MidEventCallback { private final EventTriggerService eventTriggerService; private final CaseDefinitionRepository caseDefinitionRepository; private final CaseService caseService; + private final TimeToLiveService timeToLiveService; @Autowired public MidEventCallback(CallbackInvoker callbackInvoker, UIDefinitionRepository uiDefinitionRepository, EventTriggerService eventTriggerService, @Qualifier(CachedCaseDefinitionRepository.QUALIFIER) - CaseDefinitionRepository caseDefinitionRepository, - CaseService caseService) { + CaseDefinitionRepository caseDefinitionRepository, + CaseService caseService, + TimeToLiveService timeToLiveService) { this.callbackInvoker = callbackInvoker; this.uiDefinitionRepository = uiDefinitionRepository; this.eventTriggerService = eventTriggerService; this.caseDefinitionRepository = caseDefinitionRepository; this.caseService = caseService; + this.timeToLiveService = timeToLiveService; } @Transactional @@ -85,6 +89,7 @@ public Map invoke(String caseTypeId, CaseDataContent content, } removeNextPageFieldData(currentOrNewCaseDetails, wizardPageOptional.get().getOrder(), caseTypeId, event.getEventId()); + applyTtlIncrementIfConfigured(currentOrNewCaseDetails, caseEventDefinition, caseTypeDefinition); CaseDetails caseDetailsFromMidEventCallback = callbackInvoker.invokeMidEventCallback(wizardPageOptional.get(), @@ -130,4 +135,13 @@ private CaseEventDefinition getCaseEvent(Event event, CaseTypeDefinition caseTyp } return caseEventDefinition; } + + private void applyTtlIncrementIfConfigured(CaseDetails caseDetails, + CaseEventDefinition caseEventDefinition, + CaseTypeDefinition caseTypeDefinition) { + if (timeToLiveService.isCaseTypeUsingTTL(caseTypeDefinition)) { + caseDetails.setData(timeToLiveService.updateCaseDetailsWithTTL( + caseDetails.getData(), caseEventDefinition, caseTypeDefinition)); + } + } } diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallbackTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallbackTest.java index 6c15740763..64ba99b7e0 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallbackTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallbackTest.java @@ -23,6 +23,7 @@ import uk.gov.hmcts.ccd.domain.model.definition.WizardPageField; import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent; import uk.gov.hmcts.ccd.domain.model.std.Event; +import uk.gov.hmcts.ccd.domain.service.casedeletion.TimeToLiveService; import uk.gov.hmcts.ccd.domain.service.common.CaseService; import uk.gov.hmcts.ccd.domain.service.common.EventTriggerService; import uk.gov.hmcts.ccd.domain.service.stdapi.CallbackInvoker; @@ -37,6 +38,8 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -65,6 +68,8 @@ class MidEventCallbackTest { private UIDefinitionRepository uiDefinitionRepository; @Mock private CaseService caseService; + @Mock + private TimeToLiveService timeToLiveService; @Mock private CaseEventDefinition caseEventDefinition; @@ -93,6 +98,7 @@ void setUp() { WizardPage wizardPageWithoutCallback = createWizardPage("createCase2"); given(uiDefinitionRepository.getWizardPageCollection(CASE_TYPE_ID, event.getEventId())) .willReturn(asList(wizardPageWithCallback, wizardPageWithoutCallback)); + given(timeToLiveService.isCaseTypeUsingTTL(caseTypeDefinition)).willReturn(false); } @Test @@ -439,6 +445,40 @@ void shouldFilterCaseDataContentWhenWizardPageOrderExists() { IGNORE_WARNINGS); } + @Test + @DisplayName("should apply TTL increment before invoking mid event callback for an existing case") + void shouldApplyTtlIncrementBeforeInvokingMidEventCallbackForExistingCase() { + Map populatedData = new HashMap<>(data); + populatedData.put("SystemTTLField", new TextNode("2036-05-18")); + CaseDetails existingCaseDetails = caseDetails(data); + CaseDetails populatedCaseDetails = caseDetails(populatedData); + + CaseDataContent content = newCaseDataContent() + .withEvent(event) + .withData(data) + .withCaseReference(CASE_REFERENCE) + .withIgnoreWarning(IGNORE_WARNINGS) + .build(); + + given(caseService.getCaseDetails(JURISDICTION_ID, CASE_REFERENCE)).willReturn(existingCaseDetails); + given(caseService.clone(existingCaseDetails)).willReturn(existingCaseDetails); + given(caseService.populateCurrentCaseDetailsWithEventFields(content, existingCaseDetails)) + .willReturn(populatedCaseDetails); + given(timeToLiveService.isCaseTypeUsingTTL(caseTypeDefinition)).willReturn(true); + given(timeToLiveService.updateCaseDetailsWithTTL(populatedData, caseEventDefinition, caseTypeDefinition)) + .willReturn(populatedData); + given(callbackInvoker.invokeMidEventCallback(wizardPageWithCallback, + caseTypeDefinition, + caseEventDefinition, + existingCaseDetails, + populatedCaseDetails, + IGNORE_WARNINGS)).willReturn(populatedCaseDetails); + + midEventCallback.invoke(CASE_TYPE_ID, content, "createCase1"); + + verify(timeToLiveService).updateCaseDetailsWithTTL(populatedData, caseEventDefinition, caseTypeDefinition); + } + private Map createData() { Map data = new HashMap<>(); data.put("createCase2_field1", new TextNode("test1")); From 4e3cb188e9b268e2fe581d2d78656a49daaa045c Mon Sep 17 00:00:00 2001 From: "Kiran.Yenigala" Date: Mon, 18 May 2026 17:58:08 +0100 Subject: [PATCH 13/13] CCD-7712: fix checkstyle issues --- .../ccd/domain/service/createevent/MidEventCallbackTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallbackTest.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallbackTest.java index 64ba99b7e0..ac56dfc2d8 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallbackTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/createevent/MidEventCallbackTest.java @@ -38,8 +38,6 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify;