diff --git a/README.md b/README.md index 67b8fd62..30494e45 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # FaCT Data API - [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=uk.gov.hmcts.reform%3Afact-data-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=uk.gov.hmcts.reform%3Afact-data-api) diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java index 0deef5a9..af32dfc0 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/AuthFunctionalTest.java @@ -25,6 +25,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentreContactDetails; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.DayOfTheWeek; import uk.gov.hmcts.reform.fact.data.api.entities.types.HearingEnhancementEquipment; import uk.gov.hmcts.reform.fact.data.api.entities.types.OpeningTimesDetail; @@ -524,32 +525,32 @@ void courtLocalAuthoritiesAuth() throws Exception { } @Test - @DisplayName("Court lock endpoints enforce admin-only writes") + @DisplayName("Lock endpoints enforce admin-only writes") void courtLockAuth() throws Exception { - UUID courtId = createCourtAsAdmin("Test Court Auth Lock"); + UUID courtId = createCourtAsAdmin("Test Auth Lock"); UUID userId = createUserAsAdmin("test.user.auth.lock"); - String lockPath = "/courts/" + courtId + "/v1/locks/" + Page.COURT; + String lockPath = "/locks/" + SubjectType.COURT + "/" + courtId + "/v1/" + Page.GENERAL; Response postAdmin = http.doPost(lockPath, mapper.writeValueAsString(userId), adminToken); assertThat(postAdmin.statusCode()).isIn(200, 201); assertViewerAllowed( - http.doGet("/courts/" + courtId + "/v1/locks", adminToken), - http.doGet("/courts/" + courtId + "/v1/locks", viewerToken), - "/courts/{courtId}/v1/locks [GET]" + http.doGet("/locks/" + SubjectType.COURT + "/" + courtId + "/v1/", adminToken), + http.doGet("/locks/" + SubjectType.COURT + "/" + courtId + "/v1/", viewerToken), + "/locks/{subjectType}/{subjectId}/v1 [GET]" ); assertViewerAllowed( http.doGet(lockPath, adminToken), http.doGet(lockPath, viewerToken), - "/courts/{courtId}/v1/locks/{page} [GET]" + "/locks/{subjectType}/{subjectId}/v1/{page} [GET]" ); assertViewerForbidden( http.doPost(lockPath, mapper.writeValueAsString(userId), viewerToken), - "/courts/{courtId}/v1/locks/{page} [POST]" + "/locks/{subjectType}/{subjectId}/v1/{page} [POST]" ); assertViewerForbidden( http.doDelete(lockPath, viewerToken), - "/courts/{courtId}/v1/locks/{page} [DELETE]" + "/locks/{subjectType}/{subjectId}/v1/{page} [DELETE]" ); } diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/CourtLockControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/LockControllerFunctionalTest.java similarity index 68% rename from src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/CourtLockControllerFunctionalTest.java rename to src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/LockControllerFunctionalTest.java index b78a49cd..19b062eb 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/CourtLockControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/LockControllerFunctionalTest.java @@ -8,7 +8,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; import uk.gov.hmcts.reform.fact.functional.helpers.TestDataHelper; import uk.gov.hmcts.reform.fact.functional.http.HttpClient; @@ -24,7 +25,7 @@ @Feature("Court Lock Controller") @DisplayName("Court Lock Controller") -public final class CourtLockControllerFunctionalTest { +public final class LockControllerFunctionalTest { private static final HttpClient http = new HttpClient(); private static final ObjectMapper mapper = JsonMapper.builder() @@ -32,114 +33,111 @@ public final class CourtLockControllerFunctionalTest { .build(); @Test - @DisplayName("POST /courts/{courtId}/v1/locks/{page} creates lock successfully") + @DisplayName("POST /locks/{subjectType}/{subjectId}/v1/{page} creates lock successfully") void shouldCreateCourtLockSuccessfully() throws Exception { final UUID userId = TestDataHelper.createUser(http, "test.user.create"); - final UUID courtId = TestDataHelper.createCourt(http, "Test Court Lock Create"); + final UUID courtId = TestDataHelper.createCourt(http, "Test Lock Create"); - final CourtLock createdLock = TestDataHelper.createCourtLock(http, courtId, Page.COURT, userId); + final Lock createdLock = TestDataHelper.createCourtLock(http, courtId, Page.GENERAL, userId); assertThat(createdLock.getId()) .as("Created lock should have auto-generated ID") .isNotNull(); - assertThat(createdLock.getCourtId()) + assertThat(createdLock.getSubjectId()) .as("Created lock court ID should match request court ID %s", courtId) .isEqualTo(courtId); assertThat(createdLock.getUserId()) .as("Created lock user ID should match request user ID %s", userId) .isEqualTo(userId); assertThat(createdLock.getPage()) - .as("Created lock page should match request page %s", Page.COURT) - .isEqualTo(Page.COURT); + .as("Created lock page should match request page %s", Page.GENERAL) + .isEqualTo(Page.GENERAL); assertThat(createdLock.getLockAcquired()) .as("Created lock should have lockAcquired timestamp set") .isNotNull(); } @Test - @DisplayName("GET /courts/{courtId}/v1/locks/{page} returns lock status successfully") + @DisplayName("GET /locks/{subjectType}/{subjectId}/v1/{page} returns lock status successfully") void shouldGetCourtLockStatusSuccessfully() throws Exception { final UUID userId = TestDataHelper.createUser(http, "test.user.getstatus"); - final UUID courtId = TestDataHelper.createCourt(http, "Test Court Lock Get Status"); + final UUID courtId = TestDataHelper.createCourt(http, "Test Lock Get Status"); - TestDataHelper.createCourtLock(http, courtId, Page.COURT_ACCESSIBILITY, userId); + TestDataHelper.createCourtLock(http, courtId, Page.ACCESSIBILITY, userId); final Response getLockStatusResponse = http.doGet( - "/courts/" + courtId + "/v1/locks/" + Page.COURT_ACCESSIBILITY + "/locks/" + SubjectType.COURT + "/" + courtId + "/v1/" + Page.ACCESSIBILITY ); assertThat(getLockStatusResponse.statusCode()) .as("Expected 200 OK when getting lock status for court %s page %s", - courtId, Page.COURT_ACCESSIBILITY) + courtId, Page.ACCESSIBILITY + ) .isEqualTo(OK.value()); assertThat(getLockStatusResponse.contentType()) .as("Expected JSON content type when getting lock status") .contains("json"); - final CourtLock fetchedLock = mapper.readValue( + final Lock fetchedLock = mapper.readValue( getLockStatusResponse.getBody().asString(), - CourtLock.class + Lock.class ); - assertThat(fetchedLock.getCourtId()) + assertThat(fetchedLock.getSubjectId()) .as("Fetched lock court ID should match %s", courtId) .isEqualTo(courtId); assertThat(fetchedLock.getUserId()) .as("Fetched lock user ID should match %s", userId) .isEqualTo(userId); assertThat(fetchedLock.getPage()) - .as("Fetched lock page should match %s", Page.COURT_ACCESSIBILITY) - .isEqualTo(Page.COURT_ACCESSIBILITY); + .as("Fetched lock page should match %s", Page.ACCESSIBILITY) + .isEqualTo(Page.ACCESSIBILITY); assertThat(fetchedLock.getLockAcquired()) .as("Fetched lock should have lockAcquired timestamp") .isNotNull(); } @Test - @DisplayName("DELETE /courts/{courtId}/v1/locks/{page} deletes lock successfully") + @DisplayName("DELETE /locks/{subjectType}/{subjectId}/v1/{page} deletes lock successfully") void shouldDeleteCourtLockSuccessfully() throws Exception { final UUID userId = TestDataHelper.createUser(http, "test.user.delete"); - final UUID courtId = TestDataHelper.createCourt(http, "Test Court Lock Delete"); + final UUID courtId = TestDataHelper.createCourt(http, "Test Lock Delete"); - TestDataHelper.createCourtLock(http, courtId, Page.COURT_LOCAL_AUTHORITY, userId); + TestDataHelper.createCourtLock(http, courtId, Page.LOCAL_AUTHORITIES, userId); final Response deleteLockResponse = http.doDelete( - "/courts/" + courtId + "/v1/locks/" + Page.COURT_LOCAL_AUTHORITY + "/locks/" + SubjectType.COURT + "/" + courtId + "/v1/" + Page.LOCAL_AUTHORITIES ); assertThat(deleteLockResponse.statusCode()) .as("Expected 204 NO CONTENT when deleting lock for court %s page %s", - courtId, Page.COURT_LOCAL_AUTHORITY) + courtId, Page.LOCAL_AUTHORITIES) .isEqualTo(NO_CONTENT.value()); final Response getLockStatusResponse = http.doGet( - "/courts/" + courtId + "/v1/locks/" + Page.COURT_LOCAL_AUTHORITY + "/locks/" + SubjectType.COURT + "/" + courtId + "/v1/" + Page.LOCAL_AUTHORITIES ); assertThat(getLockStatusResponse.statusCode()) - .as("Expected 200 OK when checking lock status after deletion") - .isEqualTo(OK.value()); - - assertThat(getLockStatusResponse.contentType()) - .as("Expected JSON content type when checking lock status") - .contains("json"); + .as("Expected 204 OK when checking lock status after deletion") + .isEqualTo(NO_CONTENT.value()); assertThat(getLockStatusResponse.getBody().asString()) .as("Expected null response body after lock deletion indicating no lock exists") - .isEqualTo("null"); + .isEqualTo(""); } @Test - @DisplayName("GET /courts/{courtId}/v1/locks returns all locks for a court") + @DisplayName("GET /locks/{subjectType}/{subjectId}/v1 returns all locks for a court") void shouldGetAllLocksForCourtSuccessfully() throws Exception { final UUID userId = TestDataHelper.createUser(http, "test.user.getall"); - final UUID courtId = TestDataHelper.createCourt(http, "Test Court Lock Get All"); + final UUID courtId = TestDataHelper.createCourt(http, "Test Lock Get All"); - TestDataHelper.createCourtLock(http, courtId, Page.COURT, userId); - TestDataHelper.createCourtLock(http, courtId, Page.COURT_ACCESSIBILITY, userId); + TestDataHelper.createCourtLock(http, courtId, Page.GENERAL, userId); + TestDataHelper.createCourtLock(http, courtId, Page.ACCESSIBILITY, userId); - final Response getAllLocksResponse = http.doGet("/courts/" + courtId + "/v1/locks"); + final Response getAllLocksResponse = http.doGet("/locks/" + SubjectType.COURT + "/" + courtId + "/v1"); assertThat(getAllLocksResponse.statusCode()) .as("Expected 200 OK when getting all locks for court %s", courtId) @@ -149,35 +147,35 @@ void shouldGetAllLocksForCourtSuccessfully() throws Exception { .as("Expected JSON content type when getting all locks") .contains("json"); - final List locks = getAllLocksResponse.jsonPath().getList("", CourtLock.class); + final List locks = getAllLocksResponse.jsonPath().getList("", Lock.class); assertThat(locks) - .as("Expected 2 locks for court %s", courtId) - .hasSize(2); + .as("Expected 1 locks for court %s", courtId) + .hasSize(1); assertThat(locks) .as("All locks should belong to court %s", courtId) - .allSatisfy(lock -> assertThat(lock.getCourtId()) + .allSatisfy(lock -> assertThat(lock.getSubjectId()) .as("Lock court ID should be %s", courtId) .isEqualTo(courtId)); assertThat(locks) .as("Expected locks for pages COURT and COURT_ACCESSIBILITY") - .extracting(CourtLock::getPage) - .containsExactlyInAnyOrder(Page.COURT, Page.COURT_ACCESSIBILITY); + .extracting(Lock::getPage) + .containsExactlyInAnyOrder(Page.ACCESSIBILITY); } @Test - @DisplayName("POST /courts/{courtId}/v1/locks/{page} returns 409 when page already locked by another user") + @DisplayName("POST /locks/{subjectType/{subjectId}/v1/{page} returns 409 when page already locked by another user") void shouldReturnConflictWhenPageLockedByAnotherUser() throws Exception { final UUID userAId = TestDataHelper.createUser(http, "test.user.conflict.usera"); final UUID userBId = TestDataHelper.createUser(http, "test.user.conflict.userb"); final UUID courtId = TestDataHelper.createCourt(http, "Test Court Lock Conflict"); - TestDataHelper.createCourtLock(http, courtId, Page.COURT, userAId); + TestDataHelper.createCourtLock(http, courtId, Page.GENERAL, userAId); final Response userBLockResponse = http.doPost( - "/courts/" + courtId + "/v1/locks/" + Page.COURT, + "/locks/" + SubjectType.COURT + "/" + courtId + "/v1/" + Page.GENERAL, mapper.writeValueAsString(userBId) ); @@ -199,18 +197,18 @@ void shouldReturnConflictWhenPageLockedByAnotherUser() throws Exception { } @Test - @DisplayName("POST /courts/{courtId}/v1/locks/{page} allows same user to refresh lock") + @DisplayName("POST /locks/{subjectType}/{subjectId}/v1/{page} allows same user to refresh lock") void shouldAllowSameUserToRefreshLock() throws Exception { final UUID userId = TestDataHelper.createUser(http, "test.user.refresh"); - final UUID courtId = TestDataHelper.createCourt(http, "Test Court Lock Refresh"); + final UUID courtId = TestDataHelper.createCourt(http, "Test Lock Refresh"); - final CourtLock initialLock = TestDataHelper.createCourtLock(http, courtId, Page.COURT, userId); + final Lock initialLock = TestDataHelper.createCourtLock(http, courtId, Page.GENERAL, userId); final ZonedDateTime initialTimestamp = initialLock.getLockAcquired(); Thread.sleep(1000); - final CourtLock refreshedLock = TestDataHelper.createCourtLock(http, courtId, Page.COURT, userId); + final Lock refreshedLock = TestDataHelper.createCourtLock(http, courtId, Page.GENERAL, userId); assertThat(refreshedLock.getId()) .as("Lock ID should remain the same after refresh") @@ -220,7 +218,7 @@ void shouldAllowSameUserToRefreshLock() throws Exception { .as("Lock acquired timestamp should be updated after refresh") .isAfter(initialTimestamp); - assertThat(refreshedLock.getCourtId()) + assertThat(refreshedLock.getSubjectId()) .as("Lock court ID should remain %s", courtId) .isEqualTo(courtId); @@ -229,34 +227,34 @@ void shouldAllowSameUserToRefreshLock() throws Exception { .isEqualTo(userId); assertThat(refreshedLock.getPage()) - .as("Lock page should remain %s", Page.COURT) - .isEqualTo(Page.COURT); + .as("Lock page should remain %s", Page.GENERAL) + .isEqualTo(Page.GENERAL); } @Test @DisplayName("End-to-end: Create lock, get status, refresh lock, delete, verify deletion") void shouldCompleteEndToEndLockWorkflow() throws Exception { final UUID userId = TestDataHelper.createUser(http, "test.user.endtoend"); - final UUID courtId = TestDataHelper.createCourt(http, "Test Court Lock End To End"); + final UUID courtId = TestDataHelper.createCourt(http, "Test Lock End To End"); - final CourtLock createdLock = TestDataHelper - .createCourtLock(http, courtId, Page.COURT_LOCAL_AUTHORITY, userId); + final Lock createdLock = TestDataHelper + .createCourtLock(http, courtId, Page.LOCAL_AUTHORITIES, userId); - assertThat(createdLock.getCourtId()) + assertThat(createdLock.getSubjectId()) .as("Created lock court ID should be %s", courtId) .isEqualTo(courtId); final Response getLockStatusResponse = http.doGet( - "/courts/" + courtId + "/v1/locks/" + Page.COURT_LOCAL_AUTHORITY + "/locks/" + SubjectType.COURT + "/" + courtId + "/v1/" + Page.LOCAL_AUTHORITIES ); assertThat(getLockStatusResponse.statusCode()) .as("Expected 200 OK when getting lock status") .isEqualTo(OK.value()); - final CourtLock fetchedLock = mapper.readValue( + final Lock fetchedLock = mapper.readValue( getLockStatusResponse.getBody().asString(), - CourtLock.class + Lock.class ); assertThat(fetchedLock.getId()) @@ -265,15 +263,15 @@ void shouldCompleteEndToEndLockWorkflow() throws Exception { Thread.sleep(1000); - final CourtLock refreshedLock = TestDataHelper - .createCourtLock(http, courtId, Page.COURT_LOCAL_AUTHORITY, userId); + final Lock refreshedLock = TestDataHelper + .createCourtLock(http, courtId, Page.LOCAL_AUTHORITIES, userId); assertThat(refreshedLock.getLockAcquired()) .as("Refreshed lock timestamp should be after original timestamp") .isAfter(createdLock.getLockAcquired()); final Response deleteLockResponse = http.doDelete( - "/courts/" + courtId + "/v1/locks/" + Page.COURT_LOCAL_AUTHORITY + "/locks/" + SubjectType.COURT + "/" + courtId + "/v1/" + Page.LOCAL_AUTHORITIES ); assertThat(deleteLockResponse.statusCode()) @@ -281,16 +279,16 @@ void shouldCompleteEndToEndLockWorkflow() throws Exception { .isEqualTo(NO_CONTENT.value()); final Response verifyDeletedResponse = http.doGet( - "/courts/" + courtId + "/v1/locks/" + Page.COURT_LOCAL_AUTHORITY + "/locks/" + SubjectType.COURT + "/" + courtId + "/v1/" + Page.LOCAL_AUTHORITIES ); assertThat(verifyDeletedResponse.statusCode()) - .as("Expected 200 OK when checking deleted lock status") - .isEqualTo(OK.value()); + .as("Expected 204 OK when checking deleted lock status") + .isEqualTo(NO_CONTENT.value()); assertThat(verifyDeletedResponse.getBody().asString()) .as("Expected null response after lock deletion") - .isEqualTo("null"); + .isEqualTo(""); } @AfterAll diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/UserControllerFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/UserControllerFunctionalTest.java index d4a179d8..3f990d55 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/UserControllerFunctionalTest.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/controllers/UserControllerFunctionalTest.java @@ -9,8 +9,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import uk.gov.hmcts.reform.fact.data.api.entities.Court; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; import uk.gov.hmcts.reform.fact.data.api.entities.User; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; import uk.gov.hmcts.reform.fact.functional.helpers.TestDataHelper; @@ -266,22 +267,24 @@ void shouldReturnNotFoundWhenRemovingFavouriteForNonExistentUserSuccessfully() { @DisplayName("DELETE /user/v1/{userId}/locks clears user locks successfully") void shouldReturnNoContentWhenClearingUserLocksSuccessfully() throws Exception { final UUID userId = TestDataHelper.createUser(http, "test.user.clear.locks"); + final UUID userId2 = TestDataHelper.createUser(http, "test.user2.clear.locks"); final UUID court1Id = TestDataHelper .createCourt(http, generateUniqueCourtName("Test Court Clear Locks One")); final UUID court2Id = TestDataHelper .createCourt(http, generateUniqueCourtName("Test Court Clear Locks Two")); - TestDataHelper.createCourtLock(http, court1Id, Page.COURT, userId); - TestDataHelper.createCourtLock(http, court2Id, Page.COURT_ACCESSIBILITY, userId); + TestDataHelper.createCourtLock(http, court1Id, Page.GENERAL, userId); + TestDataHelper.createCourtLock(http, court2Id, Page.ACCESSIBILITY, userId2); - final Response getLocksBeforeClearResponse = http.doGet("/courts/" + court1Id + "/v1/locks"); + final Response getLocksBeforeClearResponse = http.doGet("/locks/" + SubjectType.COURT + + "/" + court1Id + "/v1"); assertThat(getLocksBeforeClearResponse.statusCode()) .as("Expected 200 OK when getting locks before clearing") .isEqualTo(OK.value()); - final List locksBeforeClear = getLocksBeforeClearResponse.jsonPath() - .getList("", CourtLock.class); + final List locksBeforeClear = getLocksBeforeClearResponse.jsonPath() + .getList("", Lock.class); assertThat(locksBeforeClear) .as("Expected at least 1 lock before clearing for user %s", userId) @@ -293,26 +296,33 @@ void shouldReturnNoContentWhenClearingUserLocksSuccessfully() throws Exception { .as("Expected 204 NO CONTENT when clearing locks for user %s", userId) .isEqualTo(NO_CONTENT.value()); - final Response getLock1StatusResponse = http.doGet("/courts/" + court1Id + "/v1/locks/" + Page.COURT); + final Response getLock1StatusResponse = http.doGet("/locks/" + SubjectType.COURT + "/" + + court1Id + "/v1/" + Page.GENERAL); assertThat(getLock1StatusResponse.statusCode()) - .as("Expected 200 OK when checking lock status after clearing") - .isEqualTo(OK.value()); + .as("Expected 204 OK when checking lock status after clearing") + .isEqualTo(NO_CONTENT.value()); assertThat(getLock1StatusResponse.getBody().asString()) - .as("Expected null response after clearing user locks") - .isEqualTo("null"); + .as("Expected empty response after clearing user locks") + .isEqualTo(""); + + final Response clearLocksResponse2 = http.doDelete("/user/v1/" + userId2 + "/locks"); + + assertThat(clearLocksResponse.statusCode()) + .as("Expected 204 NO CONTENT when clearing locks for user %s", userId2) + .isEqualTo(NO_CONTENT.value()); - final Response getLock2StatusResponse = http.doGet("/courts/" - + court2Id + "/v1/locks/" + Page.COURT_ACCESSIBILITY); + final Response getLock2StatusResponse = http.doGet("/locks/" + SubjectType.COURT + "/" + + court2Id + "/v1/" + Page.ACCESSIBILITY); assertThat(getLock2StatusResponse.statusCode()) - .as("Expected 200 OK when checking second lock status after clearing") - .isEqualTo(OK.value()); + .as("Expected 204 OK when checking second lock status after clearing") + .isEqualTo(NO_CONTENT.value()); assertThat(getLock2StatusResponse.getBody().asString()) - .as("Expected null response for second lock after clearing user locks") - .isEqualTo("null"); + .as("Expected empty response for second lock after clearing user locks") + .isEqualTo(""); } @Test diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java index effff935..c36b776a 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/fact/functional/helpers/TestDataHelper.java @@ -6,9 +6,10 @@ import io.restassured.response.Response; import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.CourtFacilities; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.User; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; @@ -298,10 +299,10 @@ public static uk.gov.hmcts.reform.fact.data.api.entities.CourtAreasOfLaw buildCo * @param userId the user ID * @return the created court lock */ - public static CourtLock createCourtLock(final HttpClient http, final UUID courtId, - final Page page, final UUID userId) throws Exception { + public static Lock createCourtLock(final HttpClient http, final UUID courtId, + final Page page, final UUID userId) throws Exception { final Response response = http.doPost( - "/courts/" + courtId + "/v1/locks/" + page, + "/locks/" + SubjectType.COURT.name() + "/" + courtId + "/v1/" + page, mapper.writeValueAsString(userId) ); @@ -309,7 +310,7 @@ public static CourtLock createCourtLock(final HttpClient http, final UUID courtI .as("Expected 201 CREATED when creating lock for court %s page %s", courtId, page) .isEqualTo(CREATED.value()); - return mapper.readValue(response.getBody().asString(), CourtLock.class); + return mapper.readValue(response.getBody().asString(), Lock.class); } /** diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java index e9a7db14..d3a56f0a 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditingTest.java @@ -11,7 +11,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditActionType; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtPhotoRepository; @@ -142,7 +142,7 @@ void creatingAndUpdatingAServiceCentreShouldCreateAuditRecords() { Page audits = auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( serviceCentre.getId(), - AuditSubjectType.SERVICE_CENTRE, + SubjectType.SERVICE_CENTRE, CREATED_AFTER.atStartOfDay().atZone(java.time.ZoneOffset.UTC), org.springframework.data.domain.PageRequest.of(0, 1000) ); diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index bbbd0373..2e0d5834 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -11,7 +11,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditActionType; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; import uk.gov.hmcts.reform.fact.data.api.repositories.AuditRepository; import uk.gov.hmcts.reform.fact.data.api.repositories.CourtRepository; @@ -357,12 +357,12 @@ void getFilteredAndPaginatedAuditsReturnsResultsWhenSubjectTypeProvided() throws .param("pageNumber", "0") .param("pageSize", "10") .param("fromDate", LocalDate.now().toString()) - .param("subjectType", AuditSubjectType.COURT.name())) + .param("subjectType", SubjectType.COURT.name())) .andExpect(status().isOk()) .andExpect(jsonPath("$.content").isArray()) .andExpect(jsonPath("$.content.length()").value(2)) .andExpect(jsonPath("$.content[*].subjectType") - .value(allElementsEqual(AuditSubjectType.COURT.name()))) + .value(allElementsEqual(SubjectType.COURT.name()))) .andExpect(jsonPath("$.page.number").value(0)) .andExpect(jsonPath("$.page.totalElements").value(2)); } diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockControllerTest.java similarity index 53% rename from src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockControllerTest.java rename to src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockControllerTest.java index 83dcb6ff..ffc6cfc4 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockControllerTest.java @@ -10,10 +10,11 @@ import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.services.CourtLockService; +import uk.gov.hmcts.reform.fact.data.api.services.LockService; import java.util.List; import java.util.Optional; @@ -29,9 +30,9 @@ @Feature("Court Local Controller") @DisplayName("Court Local Controller") -@WebMvcTest(CourtLockController.class) +@WebMvcTest(LockController.class) @AutoConfigureMockMvc(addFilters = false) -class CourtLockControllerTest { +class LockControllerTest { @Autowired private MockMvc mockMvc; @@ -40,99 +41,103 @@ class CourtLockControllerTest { private ObjectMapper objectMapper; @MockitoBean - private CourtLockService courtLockService; + private LockService lockService; private final UUID userId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); private final UUID courtId = UUID.fromString("222e4567-e89b-12d3-a456-426614174000"); private final UUID nonExistentCourtId = UUID.fromString("11111111-1111-1111-1111-111111111111"); - private final Page testPage = Page.COURT; + private final Page testPage = Page.GENERAL; @Test - @DisplayName("GET /courts/{courtId}/v1/locks returns court locks successfully") + @DisplayName("GET /locks/{subjectType}/{subjectId}/v1/returns court locks successfully") void getCourtLocksReturnsSuccessfully() throws Exception { - List locks = List.of(new CourtLock()); - when(courtLockService.getLocksByCourtId(courtId)).thenReturn(locks); + List locks = List.of(new Lock()); + when(lockService.getAllSubjectLocks(SubjectType.COURT, courtId)).thenReturn(locks); - mockMvc.perform(get("/courts/{courtId}/v1/locks", courtId)) + mockMvc.perform(get("/locks/{subjectType}/{subjectId}/v1", SubjectType.COURT, courtId)) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()); } @Test - @DisplayName("GET /courts/{courtId}/v1/locks returns 404 if court does not exist") + @DisplayName("GET /locks/{subjectType}/{subjectId}/v1/returns 404 if court does not exist") void getCourtLocksNonExistentReturnsNotFound() throws Exception { - when(courtLockService.getLocksByCourtId(nonExistentCourtId)) + when(lockService.getAllSubjectLocks(SubjectType.COURT, nonExistentCourtId)) .thenThrow(new NotFoundException("Court not found")); - mockMvc.perform(get("/courts/{courtId}/v1/locks", nonExistentCourtId)) + mockMvc.perform(get("/locks/{subjectType}/{subjectId}/v1", SubjectType.COURT, nonExistentCourtId)) .andExpect(status().isNotFound()); } @Test - @DisplayName("GET /courts/{courtId}/v1/locks returns 400 for invalid UUID") + @DisplayName("GET /locks/{subjectType}/{subjectId}/v1/returns 400 for invalid UUID") void getCourtLocksInvalidUUID() throws Exception { - mockMvc.perform(get("/courts/{courtId}/v1/locks", "invalid-uuid")) + mockMvc.perform(get("/locks/{subjectType}/{subjectId}/v1", SubjectType.COURT, "invalid-uuid")) .andExpect(status().isBadRequest()); } @Test - @DisplayName("GET /courts/{courtId}/v1/locks/{page} returns lock status successfully") + @DisplayName("GET /locks/{subjectType}/{subjectId}/v1/{page} returns lock status successfully") void getCourtLockStatusReturnsSuccessfully() throws Exception { - CourtLock lock = new CourtLock(); - when(courtLockService.getPageLock(courtId, testPage)).thenReturn(Optional.of(lock)); + Lock lock = new Lock(); + when(lockService.getPageLock(SubjectType.COURT, courtId, testPage)).thenReturn(Optional.of(lock)); - mockMvc.perform(get("/courts/{courtId}/v1/locks/{page}", courtId, testPage)) + mockMvc.perform(get("/locks/{subjectType}/{subjectId}/v1/{page}", SubjectType.COURT, courtId, testPage)) .andExpect(status().isOk()); } @Test - @DisplayName("GET /courts/{courtId}/v1/locks/{page} returns 404 if court not found") + @DisplayName("GET /locks/{subjectType}/{subjectId}/v1/{page} returns 404 if court not found") void getCourtLockStatusNonExistentReturnsNotFound() throws Exception { - when(courtLockService.getPageLock(nonExistentCourtId, testPage)) + when(lockService.getPageLock(SubjectType.COURT, nonExistentCourtId, testPage)) .thenThrow(new NotFoundException("Court not found")); - mockMvc.perform(get("/courts/{courtId}/v1/locks/{page}", nonExistentCourtId, testPage)) + mockMvc.perform(get("/locks/{subjectType}/{subjectId}/v1/{page}", + SubjectType.COURT, nonExistentCourtId, testPage)) .andExpect(status().isNotFound()); } @Test - @DisplayName("POST /courts/{courtId}/v1/locks/{page} creates lock successfully") + @DisplayName("POST /locks/{subjectType}/{subjectId}/v1/{page} creates lock successfully") void createCourtLockSuccessfully() throws Exception { - CourtLock lock = new CourtLock(); - when(courtLockService.createOrUpdateLock(courtId, testPage, userId)).thenReturn(lock); + Lock lock = new Lock(); + when(lockService.createOrUpdateLock(SubjectType.COURT, courtId, testPage, userId)).thenReturn(lock); - mockMvc.perform(post("/courts/{courtId}/v1/locks/{page}", courtId, testPage) + mockMvc.perform(post("/locks/{subjectType}/{subjectId}/v1/{page}", SubjectType.COURT, courtId, testPage) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(userId))) .andExpect(status().isCreated()); } @Test - @DisplayName("POST /courts/{courtId}/v1/locks/{page} returns 404 if court not found") + @DisplayName("POST /locks/{subjectType}/{subjectId}/v1/{page} returns 404 if court not found") void createCourtLockNonExistentReturnsNotFound() throws Exception { - when(courtLockService.createOrUpdateLock(nonExistentCourtId, testPage, userId)) + when(lockService.createOrUpdateLock(SubjectType.COURT, nonExistentCourtId, testPage, userId)) .thenThrow(new NotFoundException("Court not found")); - mockMvc.perform(post("/courts/{courtId}/v1/locks/{page}", nonExistentCourtId, testPage) + mockMvc.perform(post("/locks/{subjectType}/{subjectId}/v1/{page}", + SubjectType.COURT, nonExistentCourtId, testPage) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(userId))) .andExpect(status().isNotFound()); } @Test - @DisplayName("DELETE /courts/{courtId}/v1/locks/{page} deletes lock successfully") + @DisplayName("DELETE /locks/{subjectType}/{subjectId}/v1/{page} deletes lock successfully") void deleteCourtLockSuccessfully() throws Exception { - mockMvc.perform(delete("/courts/{courtId}/v1/locks/{page}", courtId, testPage)) + mockMvc.perform(delete("/locks/{subjectType}/{subjectId}/v1/{page}", + SubjectType.COURT, courtId, testPage)) .andExpect(status().isNoContent()); } @Test - @DisplayName("DELETE /courts/{courtId}/v1/locks/{page} returns 404 if court or lock not found") + @DisplayName("DELETE /locks/{subjectType}/{subjectId}/v1/{page} returns 404 if court or lock not found") void deleteCourtLockNonExistentReturnsNotFound() throws Exception { doThrow(new NotFoundException("Court or lock not found")) - .when(courtLockService).deleteLock(nonExistentCourtId, testPage); + .when(lockService).deleteLock(SubjectType.COURT, nonExistentCourtId, testPage); - mockMvc.perform(delete("/courts/{courtId}/v1/locks/{page}", nonExistentCourtId, testPage)) + mockMvc.perform(delete("/locks/{subjectType}/{subjectId}/v1/{page}", + SubjectType.COURT, nonExistentCourtId, testPage)) .andExpect(status().isNotFound()); } } diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserControllerTest.java index 43721f20..995e99b5 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserControllerTest.java @@ -15,7 +15,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.entities.types.UserRole; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.services.CourtLockService; +import uk.gov.hmcts.reform.fact.data.api.services.LockService; import uk.gov.hmcts.reform.fact.data.api.services.UserService; import java.time.ZonedDateTime; @@ -48,7 +48,7 @@ class UserControllerTest { private UserService userService; @MockitoBean - private CourtLockService courtLockService; + private LockService lockService; private final UUID userId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); private final UUID nonExistentUserId = UUID.fromString("11111111-1111-1111-1111-111111111111"); @@ -161,7 +161,7 @@ void clearUserLocksSuccessfully() throws Exception { @DisplayName("DELETE /user/v1/{userId}/locks returns 404 if user not found") void clearUserLocksNonExistentUserReturnsNotFound() throws Exception { doThrow(new NotFoundException("User not found")) - .when(courtLockService).clearUserLocks(nonExistentUserId); + .when(lockService).clearUserLocks(nonExistentUserId); mockMvc.perform(delete("/user/v1/{userId}/locks", nonExistentUserId)) .andExpect(status().isNotFound()); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/validation/validator/CourtLockTimeoutValidator.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/LockAspect.java similarity index 57% rename from src/main/java/uk/gov/hmcts/reform/fact/data/api/validation/validator/CourtLockTimeoutValidator.java rename to src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/LockAspect.java index abeebf20..6cbdf123 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/validation/validator/CourtLockTimeoutValidator.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/LockAspect.java @@ -1,6 +1,7 @@ -package uk.gov.hmcts.reform.fact.data.api.validation.validator; +package uk.gov.hmcts.reform.fact.data.api.aspect; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @@ -11,57 +12,91 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ResponseStatusException; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; -import uk.gov.hmcts.reform.fact.data.api.services.CourtLockService; +import uk.gov.hmcts.reform.fact.data.api.services.LockService; import java.lang.reflect.Parameter; import java.time.Duration; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Optional; import java.util.UUID; /** - * Aspect that intercepts methods annotated with @ValidateLockTimeout. + * Aspect that intercepts methods annotated with @LockTimeoutCheck. * Validates lock timeout before allowing the method to execute. */ +@Slf4j @Aspect @Component @RequiredArgsConstructor -public class CourtLockTimeoutValidator { +public class LockAspect { - private final CourtLockService courtLockService; + private final LockService lockService; @Value("${courtLock.timeout-minutes}") private long lockTimeoutMinutes; /** - * Executes BEFORE any method annotated with @ValidateLockTimeout. - * Extracts courtId, page, and userId from method parameters and validates lock. + * Executes BEFORE any method annotated with @LockTimeoutCheck. + * Extracts subjectType, subjectId, page, and userId from method parameters and validates lock. */ - @Before("@annotation(uk.gov.hmcts.reform.fact.data.api.validation.annotations.CourtLockTimeoutCheck)") + @Before("@annotation(uk.gov.hmcts.reform.fact.data.api.aspect.annotations.LockTimeoutCheck)") public void validateLockTimeout(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Parameter[] parameters = signature.getMethod().getParameters(); Object[] args = joinPoint.getArgs(); - UUID courtId = extractUuid(parameters, args, "courtId"); + SubjectType subjectType = extractSubjectType(parameters, args, "subjectType"); + UUID subjectId = extractUuid(parameters, args, "subjectId"); Page page = extractPage(parameters, args, "page"); UUID userId = extractUuid(parameters, args, "userId"); - Optional lock = courtLockService.getPageLock(courtId, page); + Optional lock = lockService.getPageLock(subjectType, subjectId, page); if (lock.isEmpty() || lock.get().getUserId().equals(userId)) { return; // No lock or same user owns it } - long minutesLocked = Duration.between(lock.get().getLockAcquired(), ZonedDateTime.now()).toMinutes(); + long minutesLocked = Duration.between( + lock.get().getLockAcquired(), + ZonedDateTime.now(ZoneOffset.UTC) + ).toMinutes(); if (minutesLocked < lockTimeoutMinutes) { throw new ResponseStatusException(HttpStatus.CONFLICT, String.format("Page locked by another user (%d/%d min)", minutesLocked, lockTimeoutMinutes)); } - courtLockService.deleteLock(courtId, page); + lockService.deleteLock(subjectType, subjectId, page); + } + + /** + * Executes BEFORE any method annotated with @LockCleanupCheck. + */ + @Before("@annotation(uk.gov.hmcts.reform.fact.data.api.aspect.annotations.LockCleanupCheck)") + public void lockCleanup(JoinPoint joinPoint) { + try { + this.lockService.deleteExpiredLocks(); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + } + } + + /** + * Extracts SubjectType parameter by name from method arguments. + */ + private SubjectType extractSubjectType(Parameter[] parameters, Object[] args, String paramName) { + for (int i = 0; i < parameters.length; i++) { + if (isNamedParameter(parameters[i], paramName)) { + Object value = args[i]; + return value instanceof String valueStr + ? SubjectType.valueOf(valueStr.toUpperCase()) + : (SubjectType) value; + } + } + throw new IllegalArgumentException("Required parameter not found: " + paramName); } /** @@ -71,7 +106,9 @@ private UUID extractUuid(Parameter[] parameters, Object[] args, String paramName for (int i = 0; i < parameters.length; i++) { if (isNamedParameter(parameters[i], paramName)) { Object value = args[i]; - return value instanceof String ? UUID.fromString((String) value) : (UUID) value; + return value instanceof String valueStr + ? UUID.fromString(valueStr) + : (UUID) value; } } throw new IllegalArgumentException("Required parameter not found: " + paramName); @@ -84,7 +121,9 @@ private Page extractPage(Parameter[] parameters, Object[] args, String paramName for (int i = 0; i < parameters.length; i++) { if (isNamedParameter(parameters[i], paramName)) { Object value = args[i]; - return value instanceof String ? Page.valueOf(((String) value).toUpperCase()) : (Page) value; + return value instanceof String valueStr + ? Page.valueOf(valueStr.toUpperCase()) + : (Page) value; } } throw new IllegalArgumentException("Required parameter not found: " + paramName); diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/annotations/LockCleanupCheck.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/annotations/LockCleanupCheck.java new file mode 100644 index 00000000..b33a0613 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/annotations/LockCleanupCheck.java @@ -0,0 +1,14 @@ +package uk.gov.hmcts.reform.fact.data.api.aspect.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * When applied to a method, forces the lock cleanup to occur before moving on. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface LockCleanupCheck { +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/validation/annotations/CourtLockTimeoutCheck.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/annotations/LockTimeoutCheck.java similarity index 77% rename from src/main/java/uk/gov/hmcts/reform/fact/data/api/validation/annotations/CourtLockTimeoutCheck.java rename to src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/annotations/LockTimeoutCheck.java index 7fc4b822..feb75927 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/validation/annotations/CourtLockTimeoutCheck.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/aspect/annotations/LockTimeoutCheck.java @@ -1,4 +1,4 @@ -package uk.gov.hmcts.reform.fact.data.api.validation.annotations; +package uk.gov.hmcts.reform.fact.data.api.aspect.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,5 +11,5 @@ */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) -public @interface CourtLockTimeoutCheck { +public @interface LockTimeoutCheck { } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java index 85704706..fbbec373 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditController.java @@ -1,7 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.controllers; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; @@ -58,7 +58,7 @@ public ResponseEntity> getFilteredAndPaginatedAudits( @PositiveOrZero(message = "pageNumber must be greater than or equal to 0") int pageNumber, @RequestParam(name = "pageSize", defaultValue = "25") @Positive(message = "pageSize must be greater than 0") int pageSize, - @RequestParam(name = "subjectType", required = false) AuditSubjectType subjectType, + @RequestParam(name = "subjectType", required = false) SubjectType subjectType, @RequestParam(name = "courtId", required = false) @ValidUUID(allowNull = true) String courtId, @RequestParam(name = "serviceCentreId", required = false) @ValidUUID(allowNull = true) String serviceCentreId, @RequestParam(name = "email", required = false) @@ -131,7 +131,7 @@ public ResponseEntity removeExpiredAuditEntries() { @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved map of subject-> name+id pairs") }) - public ResponseEntity>> getSubjectNameAndIdMap() { + public ResponseEntity>> getSubjectNameAndIdMap() { return ResponseEntity.ok(this.auditService.getSubjectNameAndIdMap()); } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockController.java deleted file mode 100644 index 69108403..00000000 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockController.java +++ /dev/null @@ -1,100 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.controllers; - -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; -import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; -import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; -import uk.gov.hmcts.reform.fact.data.api.services.CourtLockService; -import uk.gov.hmcts.reform.fact.data.api.validation.annotations.CourtLockTimeoutCheck; -import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; - -@SecuredFactRestController( - name = "Court Lock", - description = "Operations related to court lock services" -) -@RequestMapping("/courts/{courtId}") -@SuppressWarnings("java:S4684") -public class CourtLockController { - - private final CourtLockService courtLockService; - - public CourtLockController(CourtLockService courtLockService) { - this.courtLockService = courtLockService; - } - - @GetMapping("/v1/locks") - @Operation(summary = "Get all active court locks for a court") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved court locks"), - @ApiResponse(responseCode = "400", description = "Invalid court ID supplied"), - @ApiResponse(responseCode = "404", description = "Court not found") - }) - public ResponseEntity> getCourtLocks( - @Parameter(description = "UUID of the court", required = true) @ValidUUID @PathVariable String courtId) { - return ResponseEntity.ok(courtLockService.getLocksByCourtId(UUID.fromString(courtId))); - } - - @GetMapping("/v1/locks/{page}") - @Operation(summary = "Get lock status for a specific page of a court") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved lock status"), - @ApiResponse(responseCode = "400", description = "Invalid court ID or page supplied"), - @ApiResponse(responseCode = "404", description = "Court not found") - }) - public ResponseEntity> getCourtLockStatus( - @Parameter(description = "UUID of the court", required = true) @ValidUUID @PathVariable String courtId, - @Parameter(description = "Page to check lock status", required = true) @PathVariable Page page) { - return ResponseEntity.ok(courtLockService.getPageLock(UUID.fromString(courtId), page)); - } - - @PostMapping("/v1/locks/{page}") - @CourtLockTimeoutCheck - @Operation(summary = "Create or update court lock for a specific page") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully created or updated court lock"), - @ApiResponse(responseCode = "400", description = "Invalid court ID, page or user ID supplied"), - @ApiResponse(responseCode = "404", description = "Court or user not found"), - @ApiResponse(responseCode = "409", description = "Conflict with existing court lock") - }) - @PreAuthorize("@authService.isAdmin()") - public ResponseEntity createOrUpdateCourtLock( - @Parameter(description = "UUID of the court", required = true) @ValidUUID @PathVariable String courtId, - @Parameter(description = "Page to lock", required = true) @PathVariable Page page, - @Parameter(description = "User ID creating the lock", required = true) @Valid @RequestBody UUID userId) { - return ResponseEntity.status(HttpStatus.CREATED) - .body(courtLockService.createOrUpdateLock(UUID.fromString(courtId), page, userId)); - } - - @DeleteMapping("/v1/locks/{page}") - @Operation(summary = "Delete a court lock for a specific page") - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Successfully deleted court lock"), - @ApiResponse(responseCode = "400", description = "Invalid court ID or page supplied"), - @ApiResponse(responseCode = "404", description = "Court or lock not found") - }) - @PreAuthorize("@authService.isAdmin()") - public ResponseEntity deleteCourtLock( - @Parameter(description = "UUID of the court", required = true) @ValidUUID @PathVariable String courtId, - @Parameter(description = "Page to delete lock", required = true) @PathVariable Page page) { - courtLockService.deleteLock(UUID.fromString(courtId), page); - return ResponseEntity.noContent().build(); - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockController.java new file mode 100644 index 00000000..37c07e74 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockController.java @@ -0,0 +1,110 @@ +package uk.gov.hmcts.reform.fact.data.api.controllers; + +import uk.gov.hmcts.reform.fact.data.api.aspect.annotations.LockCleanupCheck; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; +import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; +import uk.gov.hmcts.reform.fact.data.api.services.LockService; +import uk.gov.hmcts.reform.fact.data.api.aspect.annotations.LockTimeoutCheck; +import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; + +import java.util.List; +import java.util.UUID; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@SecuredFactRestController( + name = "Lock", + description = "Operations related to lock services" +) +@RequestMapping("/locks") +@SuppressWarnings("java:S4684") +public class LockController { + + private final LockService lockService; + + public LockController(LockService lockService) { + this.lockService = lockService; + } + + @LockCleanupCheck + @GetMapping("/{subjectType}/{subjectId}/v1") + @Operation(summary = "Get all active locks for a subject") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved subject locks"), + @ApiResponse(responseCode = "400", description = "Invalid subject ID supplied"), + @ApiResponse(responseCode = "404", description = "Subject not found") + }) + public ResponseEntity> getSubjectLocks( + @Parameter(description = "The subject type", required = true) @PathVariable SubjectType subjectType, + @Parameter(description = "UUID of the subject", required = true) @ValidUUID @PathVariable String subjectId) { + return ResponseEntity.ok(lockService.getAllSubjectLocks(subjectType, UUID.fromString(subjectId))); + } + + @LockCleanupCheck + @GetMapping("/{subjectType}/{subjectId}/v1/{page}") + @Operation(summary = "Get lock status for a specific page of a subject") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved lock status"), + @ApiResponse(responseCode = "204", description = "No lock data found for the specified page"), + @ApiResponse(responseCode = "400", description = "Invalid subject ID or page supplied"), + @ApiResponse(responseCode = "404", description = "Subject not found") + }) + public ResponseEntity getSubjectLockStatus( + @Parameter(description = "The subject type", required = true) @PathVariable SubjectType subjectType, + @Parameter(description = "UUID of the subject", required = true) @ValidUUID @PathVariable String subjectId, + @Parameter(description = "Page to check lock status", required = true) @PathVariable Page page) { + return lockService.getPageLock(subjectType, UUID.fromString(subjectId), page) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.noContent().build()); + } + + @LockTimeoutCheck + @PostMapping("/{subjectType}/{subjectId}/v1/{page}") + @Operation(summary = "Create or update subject lock for a specific page") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created or updated subject lock"), + @ApiResponse(responseCode = "400", description = "Invalid subject ID, page or user ID supplied"), + @ApiResponse(responseCode = "404", description = "Subject or user not found"), + @ApiResponse(responseCode = "409", description = "Conflict with existing subject lock") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity createOrUpdateSubjectLock( + @Parameter(description = "The subject type", required = true) @PathVariable SubjectType subjectType, + @Parameter(description = "UUID of the subject", required = true) @ValidUUID @PathVariable String subjectId, + @Parameter(description = "Page to lock", required = true) @PathVariable Page page, + @Parameter(description = "User ID creating the lock", required = true) @Valid @RequestBody UUID userId) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(lockService.createOrUpdateLock(subjectType, UUID.fromString(subjectId), page, userId)); + } + + @DeleteMapping("/{subjectType}/{subjectId}/v1/{page}") + @Operation(summary = "Delete a subject lock for a specific page") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Successfully deleted subject lock"), + @ApiResponse(responseCode = "400", description = "Invalid subject ID or page supplied"), + @ApiResponse(responseCode = "404", description = "Subject or lock not found") + }) + @PreAuthorize("@authService.isAdmin()") + public ResponseEntity deleteSubjectLock( + @Parameter(description = "The subject type", required = true) @PathVariable SubjectType subjectType, + @Parameter(description = "UUID of the subject", required = true) @ValidUUID @PathVariable String subjectId, + @Parameter(description = "Page to delete lock", required = true) @PathVariable Page page) { + lockService.deleteLock(subjectType, UUID.fromString(subjectId), page); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserController.java index 6437e0de..8078f869 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserController.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserController.java @@ -3,7 +3,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController; -import uk.gov.hmcts.reform.fact.data.api.services.CourtLockService; +import uk.gov.hmcts.reform.fact.data.api.services.LockService; import uk.gov.hmcts.reform.fact.data.api.services.UserService; import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID; @@ -39,11 +39,11 @@ public class UserController { private final UserService userService; - private final CourtLockService courtLockService; + private final LockService lockService; - public UserController(UserService userService, CourtLockService courtLockService) { + public UserController(UserService userService, LockService lockService) { this.userService = userService; - this.courtLockService = courtLockService; + this.lockService = lockService; } @GetMapping("/v1") @@ -127,7 +127,7 @@ public ResponseEntity deleteUserFavorite( @PreAuthorize("@authService.isAdmin()") public ResponseEntity clearUserLocks( @Parameter(description = "UUID of the user", required = true) @ValidUUID @PathVariable String userId) { - courtLockService.clearUserLocks(UUID.fromString(userId)); + lockService.clearUserLocks(UUID.fromString(userId)); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java index 4b5fbc96..d3b2cf07 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Audit.java @@ -1,7 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.entities; import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditActionType; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; import java.time.ZonedDateTime; @@ -54,7 +54,7 @@ public class Audit { @NotNull @Enumerated(EnumType.STRING) - private AuditSubjectType subjectType; + private SubjectType subjectType; @NotNull @Column(name = "user_id") diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableCourtEntity.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableCourtEntity.java index 1cf6f576..f6a225f1 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableCourtEntity.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableCourtEntity.java @@ -1,6 +1,6 @@ package uk.gov.hmcts.reform.fact.data.api.entities; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import java.util.UUID; @@ -13,7 +13,7 @@ default UUID getAuditSubjectId() { } @Override - default AuditSubjectType getAuditSubjectType() { - return AuditSubjectType.COURT; + default SubjectType getAuditSubjectType() { + return SubjectType.COURT; } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableEntity.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableEntity.java index d1438f40..8b68822f 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableEntity.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/AuditableEntity.java @@ -1,7 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.entities; import com.fasterxml.jackson.annotation.JsonIgnore; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import java.util.UUID; @@ -12,5 +12,5 @@ public interface AuditableEntity { UUID getAuditSubjectId(); @JsonIgnore - AuditSubjectType getAuditSubjectType(); + SubjectType getAuditSubjectType(); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/CourtLock.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Lock.java similarity index 71% rename from src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/CourtLock.java rename to src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Lock.java index bfeff228..10a2bec8 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/CourtLock.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/Lock.java @@ -1,11 +1,11 @@ package uk.gov.hmcts.reform.fact.data.api.entities; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; import java.time.ZonedDateTime; import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -32,8 +32,8 @@ @NoArgsConstructor @Builder @Entity -@Table(name = "court_lock") -public class CourtLock { +@Table(name = "lock") +public class Lock { @Schema( description = "The internal ID - assigned by the server during creation", @@ -43,22 +43,19 @@ public class CourtLock { @GeneratedValue(strategy = GenerationType.AUTO) private UUID id; - @Schema(description = "The ID of the associated Court", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull - @Column(name = "court_id") - private UUID courtId; + @Column(name = "subject_id") + private UUID subjectId; - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "court_id", insertable = false, updatable = false) - private Court court; + @NotNull + @Enumerated(EnumType.STRING) + private SubjectType subjectType; @Schema(description = "The ID of the associated User") @NotNull @Column(name = "user_id") private UUID userId; - @JsonIgnore @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", insertable = false, updatable = false) private User user; @@ -68,10 +65,6 @@ public class CourtLock { @Enumerated(EnumType.STRING) private Page page; - // TODO - consider using @UpdateTimestamp here to automate lock timestamping? - // as the intention is that a lock can be re-acquired to prevent - // being timed out, it may be prudent to automate the value in this field - // using an @UpdateTimestamp as well as removing the Setter. @Schema(description = "Acquire timestamp for the lock") @NotNull @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentre.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentre.java index 2542bab8..3a1dda23 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentre.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentre.java @@ -27,7 +27,7 @@ import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.type.SqlTypes; import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.CatchmentType; import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidCourtSlug; @@ -117,7 +117,7 @@ public UUID getAuditSubjectId() { } @Override - public AuditSubjectType getAuditSubjectType() { - return AuditSubjectType.SERVICE_CENTRE; + public SubjectType getAuditSubjectType() { + return SubjectType.SERVICE_CENTRE; } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java index ad95c327..7bce630b 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAddress.java @@ -25,7 +25,7 @@ import lombok.NoArgsConstructor; import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.AddressType; import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidPostcode; @@ -107,7 +107,7 @@ public UUID getAuditSubjectId() { } @Override - public AuditSubjectType getAuditSubjectType() { - return AuditSubjectType.SERVICE_CENTRE; + public SubjectType getAuditSubjectType() { + return SubjectType.SERVICE_CENTRE; } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java index 82783123..954df85d 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreAreasOfLaw.java @@ -26,7 +26,7 @@ import org.hibernate.type.SqlTypes; import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import java.util.List; import java.util.Map; @@ -115,7 +115,7 @@ public UUID getAuditSubjectId() { } @Override - public AuditSubjectType getAuditSubjectType() { - return AuditSubjectType.SERVICE_CENTRE; + public SubjectType getAuditSubjectType() { + return SubjectType.SERVICE_CENTRE; } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java index 667a3340..803f7ae4 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/ServiceCentreContactDetails.java @@ -26,7 +26,7 @@ import lombok.NoArgsConstructor; import uk.gov.hmcts.reform.fact.data.api.audit.AuditableCourtEntityListener; import uk.gov.hmcts.reform.fact.data.api.controllers.ServiceCentreController.ServiceCentreDetailsView; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.validation.ValidationConstants; import java.util.UUID; @@ -122,7 +122,7 @@ public UUID getAuditSubjectId() { } @Override - public AuditSubjectType getAuditSubjectType() { - return AuditSubjectType.SERVICE_CENTRE; + public SubjectType getAuditSubjectType() { + return SubjectType.SERVICE_CENTRE; } } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/Page.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/Page.java index 9f97c569..6ca3eb73 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/Page.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/Page.java @@ -1,7 +1,18 @@ package uk.gov.hmcts.reform.fact.data.api.entities.types; public enum Page { - COURT, - COURT_ACCESSIBILITY, - COURT_LOCAL_AUTHORITY + GENERAL, + ADDRESS, + ACCESSIBILITY, + CASES_HEARD, + CONTACT_DETAILS, + COUNTER_SERVICE_OPENING_HOURS, + OPENING_HOURS, + BUILDING_FACILITIES, + INFORMATION_FOR_PROFESSIONALS, + LOCAL_AUTHORITIES, + PHOTO, + TRANSLATION_AND_INTERPRETATION, + SINGLE_POINT_OF_ENTRY, + WARNING_NOTICE; } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SubjectType.java similarity index 82% rename from src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java rename to src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SubjectType.java index 4427dcf3..532a4418 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/AuditSubjectType.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/entities/types/SubjectType.java @@ -3,7 +3,7 @@ /** * Subject types that are applicable for auditing. */ -public enum AuditSubjectType { +public enum SubjectType { COURT, SERVICE_CENTRE } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java index 114d7a1c..99fea321 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/AuditRepository.java @@ -1,7 +1,7 @@ package uk.gov.hmcts.reform.fact.data.api.repositories; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import java.time.ZonedDateTime; import java.util.List; @@ -34,20 +34,20 @@ Page findByCreatedAtBetween( @EntityGraph(attributePaths = {"user"}) Page findBySubjectTypeAndCreatedAtAfter( - AuditSubjectType subjectType, + SubjectType subjectType, ZonedDateTime createdAtAfter, Pageable pageable); @EntityGraph(attributePaths = {"user"}) Page findBySubjectIdAndSubjectTypeAndCreatedAtAfter( UUID subjectId, - AuditSubjectType subjectType, + SubjectType subjectType, ZonedDateTime createdAt, Pageable pageable); @EntityGraph(attributePaths = {"user"}) Page findBySubjectTypeAndCreatedAtBetween( - AuditSubjectType subjectType, + SubjectType subjectType, ZonedDateTime createdAtAfter, ZonedDateTime createdAtBefore, Pageable pageable); @@ -55,7 +55,7 @@ Page findBySubjectTypeAndCreatedAtBetween( @EntityGraph(attributePaths = {"user"}) Page findBySubjectIdAndSubjectTypeAndCreatedAtBetween( UUID subjectId, - AuditSubjectType subjectType, + SubjectType subjectType, ZonedDateTime createdAtAfter, ZonedDateTime createdAtBefore, Pageable pageable); @@ -105,7 +105,7 @@ Page findByCreatedAtBetweenAndEmailAddressLike( @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER + AND_SUBJECT_EQUALS) Page findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( UUID subjectId, - AuditSubjectType subjectType, + SubjectType subjectType, ZonedDateTime createdAtAfter, String email, Pageable pageable); @@ -114,7 +114,7 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_BETWEEN + AND_SUBJECT_EQUALS) Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( UUID subjectId, - AuditSubjectType subjectType, + SubjectType subjectType, ZonedDateTime createdAtAfter, ZonedDateTime createdAtBefore, String email, @@ -123,7 +123,7 @@ Page findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( @EntityGraph(attributePaths = {"user"}) @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_AFTER + AND_SUBJECT_TYPE_EQUALS) Page findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( - AuditSubjectType subjectType, + SubjectType subjectType, ZonedDateTime createdAtAfter, String email, Pageable pageable); @@ -131,7 +131,7 @@ Page findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( @EntityGraph(attributePaths = {"user"}) @Query(value = SELECT_WITH_USER_JOIN + WHERE_EMAIL_LIKE_AND_CREATED_AT_BETWEEN + AND_SUBJECT_TYPE_EQUALS) Page findBySubjectTypeAndCreatedAtBetweenAndEmailAddressLike( - AuditSubjectType subjectType, + SubjectType subjectType, ZonedDateTime createdAtAfter, ZonedDateTime createdAtBefore, String email, diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtLockRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtLockRepository.java deleted file mode 100644 index 73fd09ef..00000000 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/CourtLockRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.repositories; - -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; - -@Repository -public interface CourtLockRepository extends JpaRepository { - void deleteAllByUserId(UUID userId); - - List findAllByCourtId(UUID userId); - - Optional findByCourtIdAndPage(UUID courtId, Page page); - - void deleteByCourtIdAndPage(UUID courtId, Page page); -} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/LockRepository.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/LockRepository.java new file mode 100644 index 00000000..61198975 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/repositories/LockRepository.java @@ -0,0 +1,111 @@ +package uk.gov.hmcts.reform.fact.data.api.repositories; + +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; + +@Repository +public interface LockRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"user"}) + List findAllBySubjectTypeAndSubjectId(SubjectType subjectType, UUID subjectId); + + @EntityGraph(attributePaths = {"user"}) + Optional findBySubjectTypeAndSubjectIdAndPage(SubjectType subjectType, UUID subjectId, Page page); + + @Modifying + @Query(value = """ + DELETE FROM + "lock" + WHERE + subject_type = :subjectType + AND + subject_id = :subjectId + AND + page = :page + """, nativeQuery = true) + void deleteBySubjectTypeAndSubjectIdAndPage( + @Param("subjectType") String subjectType, + @Param("subjectId") UUID subjectId, + @Param("page") String page + ); + + @Modifying + @Query(value = """ + DELETE FROM + "lock" + WHERE + lock_acquired < :lockAcquired + """, nativeQuery = true) + void deleteByLockAcquiredBefore( + @Param("lockAcquired") ZonedDateTime lockAcquired + ); + + @Modifying + @Query(value = """ + DELETE FROM + "lock" + WHERE + user_id = :userId + """, nativeQuery = true) + void deleteAllByUserId( + @Param("userId") UUID userId + ); + + @Query(value = """ + WITH updated AS ( + UPDATE "lock" + SET user_id = :userId, + lock_acquired = :lockAcquired + WHERE subject_type = :subjectType + AND subject_id = :subjectId + AND page = :page + RETURNING id + ), + inserted AS ( + INSERT INTO "lock" (id, subject_id, subject_type, user_id, page, lock_acquired) + SELECT :newLockId, :subjectId, :subjectType, :userId, :page, :lockAcquired + WHERE NOT EXISTS (SELECT 1 FROM updated) + RETURNING id + ), + kept AS ( + SELECT id FROM updated + UNION ALL + SELECT id FROM inserted + ), + remove_duplicate_page_locks AS ( + DELETE FROM "lock" + WHERE subject_type = :subjectType + AND subject_id = :subjectId + AND page = :page + AND id NOT IN (SELECT id FROM kept) + ), + remove_other_user_locks AS ( + DELETE FROM "lock" + WHERE user_id = :userId + AND id NOT IN (SELECT id FROM kept) + ) + SELECT id FROM kept LIMIT 1 + """, nativeQuery = true) + UUID upsertLockAndDeleteOtherUserLocks( + @Param("newLockId") UUID newLockId, + @Param("subjectType") String subjectType, + @Param("subjectId") UUID subjectId, + @Param("page") String page, + @Param("userId") UUID userId, + @Param("lockAcquired") ZonedDateTime lockAcquired + ); +} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java index 680aed57..a77feddb 100644 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AuditService.java @@ -2,7 +2,7 @@ import uk.gov.hmcts.reform.fact.data.api.config.properties.AuditConfigurationProperties; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; @@ -47,7 +47,7 @@ public class AuditService { * @return a {@link Page} of {@link Audit} results. */ public Page getFilteredAndPaginatedAudits(int pageNumber, int pageSize, @NonNull LocalDate fromDate, - LocalDate toDate, AuditSubjectType subjectType, String courtId, + LocalDate toDate, SubjectType subjectType, String courtId, String serviceCentreId, String email) { Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); @@ -109,7 +109,7 @@ private Page performAuditQuery(ZonedDateTime fromDateTime, ZonedDateTime private Page performSubjectAuditQuery(ZonedDateTime fromDateTime, ZonedDateTime toDateTime, SubjectFilter subjectFilter, String email, Pageable pageable) { UUID subjectId = subjectFilter.subjectId(); - AuditSubjectType subjectType = subjectFilter.subjectType(); + SubjectType subjectType = subjectFilter.subjectType(); boolean hasToDate = toDateTime != null; boolean hasEmail = email != null && !email.isBlank(); @@ -130,7 +130,7 @@ private Page performSubjectAuditQuery(ZonedDateTime fromDateTime, ZonedDa } private Page performSubjectTypeAuditQuery(ZonedDateTime fromDateTime, ZonedDateTime toDateTime, - AuditSubjectType subjectType, String email, Pageable pageable) { + SubjectType subjectType, String email, Pageable pageable) { boolean hasToDate = toDateTime != null; boolean hasEmail = email != null && !email.isBlank(); @@ -151,15 +151,15 @@ private Page performSubjectTypeAuditQuery(ZonedDateTime fromDateTime, Zon } private static SubjectFilter resolveSubjectFilter(String courtId, String serviceCentreId, - AuditSubjectType subjectType) { + SubjectType subjectType) { if (courtId != null && serviceCentreId != null) { throw new InvalidParameterCombinationException("Only one of courtId or serviceCentreId can be provided"); } if (courtId != null) { - return new SubjectFilter(UUID.fromString(courtId), AuditSubjectType.COURT); + return new SubjectFilter(UUID.fromString(courtId), SubjectType.COURT); } if (serviceCentreId != null) { - return new SubjectFilter(UUID.fromString(serviceCentreId), AuditSubjectType.SERVICE_CENTRE); + return new SubjectFilter(UUID.fromString(serviceCentreId), SubjectType.SERVICE_CENTRE); } // if a subject type is provided without a specific subject id, we can still filter by subject type if (subjectType != null) { @@ -168,18 +168,18 @@ private static SubjectFilter resolveSubjectFilter(String courtId, String service return null; } - private record SubjectFilter(UUID subjectId, AuditSubjectType subjectType) {} + private record SubjectFilter(UUID subjectId, SubjectType subjectType) {} /** * Retrieves the complete set of supported audit subjects with their corresponding name and id pairs. * - * @return a {@link Map} of {@link AuditSubjectType} to a list of {@link NameAndId} pairs + * @return a {@link Map} of {@link SubjectType} to a list of {@link NameAndId} pairs * representing the names and ids of the subjects. */ - public Map> getSubjectNameAndIdMap() { + public Map> getSubjectNameAndIdMap() { return Map.of( - AuditSubjectType.COURT, courtService.getAllCourtNameAndIds(), - AuditSubjectType.SERVICE_CENTRE, serviceCentreService.getAllServiceCentreNameAndIds() + SubjectType.COURT, courtService.getAllCourtNameAndIds(), + SubjectType.SERVICE_CENTRE, serviceCentreService.getAllServiceCentreNameAndIds() ); } diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtLockService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtLockService.java deleted file mode 100644 index 58f32482..00000000 --- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtLockService.java +++ /dev/null @@ -1,106 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.services; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtLockRepository; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; -import uk.gov.hmcts.reform.fact.data.api.entities.Court; -import uk.gov.hmcts.reform.fact.data.api.entities.User; - -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -/** - * Service class responsible for managing court lock operations. - * Provides functionality to handle court locking mechanisms and user-specific locks. - */ -@Service -public class CourtLockService { - - private final CourtLockRepository courtLockRepository; - private final UserService userService; - private final CourtService courtService; - - /** - * Constructs a new CourtLockService with required dependencies. - * - * @param courtLockRepository Repository for court lock operations - * @param courtService Service for court operations - * @param userService Service for user operations - */ - public CourtLockService(CourtLockRepository courtLockRepository, - CourtService courtService, - UserService userService) { - this.courtLockRepository = courtLockRepository; - this.courtService = courtService; - this.userService = userService; - } - - /** - * Delete all court locks for a given user id. - * - * @param userId The user id to clear locks for. - */ - @Transactional - public void clearUserLocks(UUID userId) { - courtLockRepository.deleteAllByUserId(userService.getUserById(userId).getId()); - } - - /** - * Gets all active locks for a specific court. - * - * @param courtId The court's unique identifier - * @return List of active court locks with court ID matching the provided ID - */ - public List getLocksByCourtId(UUID courtId) { - return courtLockRepository.findAllByCourtId(courtService.getCourtById(courtId).getId()); - } - - /** - * Checks if a specific page is locked for a court. - * - * @param courtId The court's unique identifier - * @param page The page to check - * @return Optional containing the lock if it exists - */ - public Optional getPageLock(UUID courtId, Page page) { - return courtLockRepository.findByCourtIdAndPage(courtService.getCourtById(courtId).getId(), page); - } - - /** - * Updates an existing court lock. - * - * @param courtId The court's unique identifier - * @param page The page to update - * @param userId The user's unique identifier - * @return The updated court lock - */ - public CourtLock createOrUpdateLock(UUID courtId, Page page, UUID userId) { - Court court = courtService.getCourtById(courtId); - User user = userService.getUserById(userId); - - CourtLock courtLock = courtLockRepository.findByCourtIdAndPage(court.getId(), page) - .orElse(new CourtLock()); - - courtLock.setCourtId(court.getId()); - courtLock.setUserId(user.getId()); - courtLock.setPage(page); - courtLock.setLockAcquired(ZonedDateTime.now()); - - return courtLockRepository.save(courtLock); - } - - /** - * Deletes a specific court lock. - * - * @param courtId The court's unique identifier - * @param page The page to unlock - */ - @Transactional - public void deleteLock(UUID courtId, Page page) { - courtLockRepository.deleteByCourtIdAndPage(courtService.getCourtById(courtId).getId(), page); - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/LockService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/LockService.java new file mode 100644 index 00000000..d2a2ef22 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/LockService.java @@ -0,0 +1,145 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; +import uk.gov.hmcts.reform.fact.data.api.repositories.LockRepository; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; +import uk.gov.hmcts.reform.fact.data.api.entities.User; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Service class responsible for managing court lock operations. + * Provides functionality to handle court locking mechanisms and user-specific locks. + */ +@Service +@RequiredArgsConstructor +public class LockService { + + @Value("${courtLock.timeout-minutes}") + private long lockTimeoutMinutes; + + private final LockRepository lockRepository; + private final UserService userService; + private final CourtService courtService; + private final ServiceCentreService serviceCentreService; + + /** + * Delete all court locks for a given user id. + * + * @param userId The user id to clear locks for. + */ + @Transactional + public void clearUserLocks(UUID userId) { + lockRepository.deleteAllByUserId(userService.getUserById(userId).getId()); + } + + /** + * get all active locks for the given subject. + * + * @param subjectType the subject type to get locks for + * @param subjectId the id of the subject + * @return the {@link List} of {@link Lock}s for the given subject + */ + public List getAllSubjectLocks(SubjectType subjectType, UUID subjectId) { + UUID id = verifySubject(subjectType, subjectId); + + return lockRepository.findAllBySubjectTypeAndSubjectId(subjectType,id); + } + + /** + * Checks if a specific page is locked for a subject. + * + * @param subjectType the subject type to get locks for + * @param subjectId the id of the subject + * @param page The page to check + * @return Optional containing the lock if it exists + */ + public Optional getPageLock(SubjectType subjectType, UUID subjectId, Page page) { + UUID id = verifySubject(subjectType, subjectId); + + return lockRepository.findBySubjectTypeAndSubjectIdAndPage(subjectType, id, page); + } + + /** + * Updates an existing subject lock. + * + * @param subjectType the subject type to get locks for + * @param subjectId the id of the subject + * @param page The page to update + * @param userId The user's unique identifier + * @return The updated court lock + */ + @Transactional + public Lock createOrUpdateLock(SubjectType subjectType, UUID subjectId, Page page, UUID userId) { + UUID id = verifySubject(subjectType, subjectId); + + User user = userService.getUserById(userId); + ZonedDateTime lockAcquired = ZonedDateTime.now(ZoneOffset.UTC); + + UUID lockId = lockRepository.upsertLockAndDeleteOtherUserLocks( + UUID.randomUUID(), + subjectType.name(), + id, + page.name(), + user.getId(), + lockAcquired + ); + + return Lock.builder() + .id(lockId) + .subjectId(id) + .subjectType(subjectType) + .userId(user.getId()) + .user(user) + .page(page) + .lockAcquired(lockAcquired) + .build(); + } + + /** + * Deletes a specific subject lock. + * + * @param subjectType the subject type to get locks for + * @param subjectId the id of the subject + * @param page The page to unlock + */ + @Transactional + public void deleteLock(SubjectType subjectType, UUID subjectId, Page page) { + UUID id = verifySubject(subjectType, subjectId); + + lockRepository.deleteBySubjectTypeAndSubjectIdAndPage(subjectType.name(), id, page.name()); + } + + /** + * Deletes all expired locks. + */ + @Transactional + public void deleteExpiredLocks() { + lockRepository.deleteByLockAcquiredBefore(ZonedDateTime.now(ZoneOffset.UTC).minusMinutes(lockTimeoutMinutes)); + } + + /** + * Verifies the subject type and id, returning the corresponding subject id. + * + * @param subjectType the {@link SubjectType} + * @param subjectId the id + * @return the subject id as a UUID + * + * @throws uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException if the entity does not exist + */ + private UUID verifySubject(final SubjectType subjectType, final UUID subjectId) { + return subjectType == SubjectType.SERVICE_CENTRE + ? serviceCentreService.getServiceCentreById(subjectId).getId() + : courtService.getCourtById(subjectId).getId(); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e8d135ee..5ecbd671 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -92,7 +92,7 @@ user: retention-period: 365 courtLock: - timeout-minutes: 60 + timeout-minutes: ${COURT_LOCK_TIMEOUT_MINUTES:60} os: key: ${OS_KEY} diff --git a/src/main/resources/db/migration/V1.40__generalise_lock_subject.sql b/src/main/resources/db/migration/V1.40__generalise_lock_subject.sql new file mode 100644 index 00000000..6bc3a7ae --- /dev/null +++ b/src/main/resources/db/migration/V1.40__generalise_lock_subject.sql @@ -0,0 +1,20 @@ +-- remove the foreign key constraint on court_lock +ALTER TABLE court_lock + DROP CONSTRAINT fk_court_lock_court; + +-- rename court_lock to lock and add subject_type column to generalise +-- its usage for both courts and service centres +ALTER TABLE court_lock RENAME TO lock; + +ALTER TABLE lock + RENAME COLUMN court_id TO subject_id; + +ALTER TABLE lock + ADD COLUMN subject_type VARCHAR; + +UPDATE lock +SET subject_type = 'COURT' +WHERE subject_type IS NULL; + +ALTER TABLE lock + ALTER COLUMN subject_type SET NOT NULL; diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/validator/CourtLockTimeoutValidatorTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/aspect/LockAspectTest.java similarity index 62% rename from src/test/java/uk/gov/hmcts/reform/fact/data/api/validator/CourtLockTimeoutValidatorTest.java rename to src/test/java/uk/gov/hmcts/reform/fact/data/api/aspect/LockAspectTest.java index 42b8096c..18105aaa 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/validator/CourtLockTimeoutValidatorTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/aspect/LockAspectTest.java @@ -1,4 +1,4 @@ -package uk.gov.hmcts.reform.fact.data.api.validator; +package uk.gov.hmcts.reform.fact.data.api.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; @@ -13,10 +13,10 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ResponseStatusException; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; -import uk.gov.hmcts.reform.fact.data.api.services.CourtLockService; -import uk.gov.hmcts.reform.fact.data.api.validation.validator.CourtLockTimeoutValidator; +import uk.gov.hmcts.reform.fact.data.api.services.LockService; import java.lang.reflect.Method; import java.time.ZonedDateTime; @@ -27,15 +27,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class CourtLockTimeoutValidatorTest { +class LockAspectTest { @Mock - private CourtLockService courtLockService; + private LockService lockService; @Mock private JoinPoint joinPoint; @@ -43,16 +45,16 @@ class CourtLockTimeoutValidatorTest { @Mock private MethodSignature methodSignature; - private CourtLockTimeoutValidator validator; + private LockAspect validator; private final UUID courtId = UUID.randomUUID(); private final UUID userId = UUID.randomUUID(); private final UUID otherUserId = UUID.randomUUID(); - private final Page page = Page.COURT; + private final Page page = Page.GENERAL; @BeforeEach void setUp() { - validator = new CourtLockTimeoutValidator(courtLockService); + validator = new LockAspect(lockService); ReflectionTestUtils.setField(validator, "lockTimeoutMinutes", 60L); } @@ -60,29 +62,29 @@ void setUp() { @DisplayName("Should allow operation when no lock exists") void shouldAllowWhenNoLockExists() { setupJoinPoint(courtId, page, userId); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.empty()); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.empty()); assertDoesNotThrow(() -> validator.validateLockTimeout(joinPoint)); - verify(courtLockService, never()).deleteLock(any(), any()); + verify(lockService, never()).deleteLock(any(), any(), any()); } @Test @DisplayName("Should allow operation when same user owns the lock") void shouldAllowWhenSameUserOwnsLock() { setupJoinPoint(courtId, page, userId); - CourtLock lock = createLock(userId, ZonedDateTime.now().minusMinutes(30)); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.of(lock)); + Lock lock = createLock(userId, ZonedDateTime.now().minusMinutes(30)); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.of(lock)); assertDoesNotThrow(() -> validator.validateLockTimeout(joinPoint)); - verify(courtLockService, never()).deleteLock(any(), any()); + verify(lockService, never()).deleteLock(any(), any(), any()); } @Test @DisplayName("Should throw 409 CONFLICT when lock is valid and held by another user") void shouldThrowConflictWhenLockIsValid() { setupJoinPoint(courtId, page, userId); - CourtLock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(30)); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.of(lock)); + Lock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(30)); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.of(lock)); ResponseStatusException exception = assertThrows( ResponseStatusException.class, @@ -92,37 +94,37 @@ void shouldThrowConflictWhenLockIsValid() { // Then assertEquals(HttpStatus.CONFLICT, exception.getStatusCode()); assertEquals("Page locked by another user (30/60 min)", exception.getReason()); - verify(courtLockService, never()).deleteLock(any(), any()); + verify(lockService, never()).deleteLock(any(), any(), any()); } @Test @DisplayName("Should delete stale lock and allow operation when lock has exceeded timeout") void shouldDeleteStaleLockAndAllow() { setupJoinPoint(courtId, page, userId); - CourtLock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(61)); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.of(lock)); + Lock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(61)); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.of(lock)); assertDoesNotThrow(() -> validator.validateLockTimeout(joinPoint)); - verify(courtLockService).deleteLock(courtId, page); + verify(lockService).deleteLock(SubjectType.COURT, courtId, page); } @Test @DisplayName("Should delete lock exactly at timeout threshold") void shouldDeleteLockAtExactTimeout() { setupJoinPoint(courtId, page, userId); - CourtLock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(60)); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.of(lock)); + Lock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(60)); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.of(lock)); assertDoesNotThrow(() -> validator.validateLockTimeout(joinPoint)); - verify(courtLockService).deleteLock(courtId, page); + verify(lockService).deleteLock(SubjectType.COURT, courtId, page); } @Test @DisplayName("Should not delete lock just before timeout threshold") void shouldNotDeleteLockJustBeforeTimeout() { setupJoinPoint(courtId, page, userId); - CourtLock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(59)); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.of(lock)); + Lock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(59)); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.of(lock)); ResponseStatusException exception = assertThrows( ResponseStatusException.class, @@ -130,18 +132,18 @@ void shouldNotDeleteLockJustBeforeTimeout() { ); assertEquals(HttpStatus.CONFLICT, exception.getStatusCode()); - verify(courtLockService, never()).deleteLock(any(), any()); + verify(lockService, never()).deleteLock(any(), any(), any()); } @Test @DisplayName("Should handle very old stale locks") void shouldHandleVeryOldStaleLocks() { setupJoinPoint(courtId, page, userId); - CourtLock lock = createLock(otherUserId, ZonedDateTime.now().minusHours(24)); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.of(lock)); + Lock lock = createLock(otherUserId, ZonedDateTime.now().minusHours(24)); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.of(lock)); assertDoesNotThrow(() -> validator.validateLockTimeout(joinPoint)); - verify(courtLockService).deleteLock(courtId, page); + verify(lockService).deleteLock(SubjectType.COURT, courtId, page); } @Test @@ -149,11 +151,11 @@ void shouldHandleVeryOldStaleLocks() { void shouldRespectCustomTimeout() { ReflectionTestUtils.setField(validator, "lockTimeoutMinutes", 15L); setupJoinPoint(courtId, page, userId); - CourtLock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(20)); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.of(lock)); + Lock lock = createLock(otherUserId, ZonedDateTime.now().minusMinutes(20)); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.of(lock)); assertDoesNotThrow(() -> validator.validateLockTimeout(joinPoint)); - verify(courtLockService).deleteLock(courtId, page); + verify(lockService).deleteLock(SubjectType.COURT, courtId, page); } @Test @@ -165,7 +167,7 @@ void shouldThrowWhenCourtIdMissing() { IllegalArgumentException.class, () -> validator.validateLockTimeout(joinPoint) ); - assertEquals("Required parameter not found: courtId", exception.getMessage()); + assertEquals("Required parameter not found: subjectType", exception.getMessage()); } @Test @@ -196,7 +198,7 @@ void shouldThrowWhenUserIdMissing() { @DisplayName("Should handle courtId as String and convert to UUID") void shouldHandleCourtIdAsString() { setupJoinPointWithStringCourtId(courtId.toString(), page, userId); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.empty()); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.empty()); assertDoesNotThrow(() -> validator.validateLockTimeout(joinPoint)); } @@ -204,29 +206,50 @@ void shouldHandleCourtIdAsString() { @Test @DisplayName("Should handle page as String and convert to enum") void shouldHandlePageAsString() { - setupJoinPointWithStringPage(courtId, "COURT", userId); - when(courtLockService.getPageLock(courtId, page)).thenReturn(Optional.empty()); + setupJoinPointWithStringPage(courtId, "GENERAL", userId); + when(lockService.getPageLock(SubjectType.COURT, courtId, page)).thenReturn(Optional.empty()); assertDoesNotThrow(() -> validator.validateLockTimeout(joinPoint)); } + @Test + @DisplayName("Should call deleteExpiredLocks when LockCleanupCheck advice runs") + void shouldCleanupExpiredLocks() { + assertDoesNotThrow(() -> validator.lockCleanup(joinPoint)); + + verify(lockService, times(1)).deleteExpiredLocks(); + } + + @Test + @DisplayName("Should swallow exception when deleteExpiredLocks fails") + void shouldSwallowCleanupException() { + doThrow(new RuntimeException("cleanup failed")).when(lockService).deleteExpiredLocks(); + + assertDoesNotThrow(() -> validator.lockCleanup(joinPoint)); + + verify(lockService, times(1)).deleteExpiredLocks(); + } + // Helper methods - private CourtLock createLock(UUID userId, ZonedDateTime lockAcquired) { - CourtLock lock = new CourtLock(); + private Lock createLock(UUID userId, ZonedDateTime lockAcquired) { + Lock lock = new Lock(); lock.setUserId(userId); lock.setLockAcquired(lockAcquired); - lock.setCourtId(courtId); + lock.setSubjectType(SubjectType.COURT); + lock.setSubjectId(courtId); lock.setPage(page); return lock; } private void setupJoinPoint(UUID courtId, Page page, UUID userId) { try { - Method method = TestController.class.getMethod("testMethod", UUID.class, Page.class, UUID.class); + Method method = TestController.class.getMethod("testMethod", + SubjectType.class, UUID.class, Page.class, UUID.class); when(joinPoint.getSignature()).thenReturn(methodSignature); when(methodSignature.getMethod()).thenReturn(method); - when(joinPoint.getArgs()).thenReturn(new Object[]{courtId, page, userId}); + when(joinPoint.getArgs()).thenReturn(new Object[]{ + SubjectType.COURT, courtId, page, userId}); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } @@ -235,10 +258,10 @@ private void setupJoinPoint(UUID courtId, Page page, UUID userId) { private void setupJoinPointWithStringCourtId(String courtId, Page page, UUID userId) { try { Method method = TestController.class.getMethod( - "testMethodWithStringCourtId", String.class, Page.class, UUID.class); + "testMethodWithStringSubjectId", SubjectType.class, String.class, Page.class, UUID.class); when(joinPoint.getSignature()).thenReturn(methodSignature); when(methodSignature.getMethod()).thenReturn(method); - when(joinPoint.getArgs()).thenReturn(new Object[]{courtId, page, userId}); + when(joinPoint.getArgs()).thenReturn(new Object[]{SubjectType.COURT, courtId, page, userId}); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } @@ -247,10 +270,10 @@ private void setupJoinPointWithStringCourtId(String courtId, Page page, UUID use private void setupJoinPointWithStringPage(UUID courtId, String page, UUID userId) { try { Method method = TestController.class.getMethod( - "testMethodWithStringPage", UUID.class, String.class, UUID.class); + "testMethodWithStringPage", SubjectType.class, UUID.class, String.class, UUID.class); when(joinPoint.getSignature()).thenReturn(methodSignature); when(methodSignature.getMethod()).thenReturn(method); - when(joinPoint.getArgs()).thenReturn(new Object[]{courtId, page, userId}); + when(joinPoint.getArgs()).thenReturn(new Object[]{SubjectType.COURT, courtId, page, userId}); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } @@ -258,7 +281,7 @@ private void setupJoinPointWithStringPage(UUID courtId, String page, UUID userId private void setupJoinPointWithoutCourtId(Page page, UUID userId) { try { - Method method = TestController.class.getMethod("testMethodWithoutCourtId", Page.class, UUID.class); + Method method = TestController.class.getMethod("testMethodWithoutSubject", Page.class, UUID.class); when(joinPoint.getSignature()).thenReturn(methodSignature); when(methodSignature.getMethod()).thenReturn(method); when(joinPoint.getArgs()).thenReturn(new Object[]{page, userId}); @@ -269,10 +292,11 @@ private void setupJoinPointWithoutCourtId(Page page, UUID userId) { private void setupJoinPointWithoutPage(UUID courtId, UUID userId) { try { - Method method = TestController.class.getMethod("testMethodWithoutPage", UUID.class, UUID.class); + Method method = TestController.class.getMethod("testMethodWithoutPage", + SubjectType.class, UUID.class, UUID.class); when(joinPoint.getSignature()).thenReturn(methodSignature); when(methodSignature.getMethod()).thenReturn(method); - when(joinPoint.getArgs()).thenReturn(new Object[]{courtId, userId}); + when(joinPoint.getArgs()).thenReturn(new Object[]{SubjectType.COURT, courtId, userId}); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } @@ -280,10 +304,11 @@ private void setupJoinPointWithoutPage(UUID courtId, UUID userId) { private void setupJoinPointWithoutUserId(UUID courtId, Page page) { try { - Method method = TestController.class.getMethod("testMethodWithoutUserId", UUID.class, Page.class); + Method method = TestController.class.getMethod("testMethodWithoutUserId", + SubjectType.class, UUID.class, Page.class); when(joinPoint.getSignature()).thenReturn(methodSignature); when(methodSignature.getMethod()).thenReturn(method); - when(joinPoint.getArgs()).thenReturn(new Object[]{courtId, page}); + when(joinPoint.getArgs()).thenReturn(new Object[]{SubjectType.COURT, courtId, page}); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } @@ -293,37 +318,47 @@ private void setupJoinPointWithoutUserId(UUID courtId, Page page) { @SuppressWarnings("unused") static class TestController { public void testMethod( - @PathVariable("courtId") UUID courtId, + @PathVariable("subjectType") SubjectType subjectType, + @PathVariable("subjectId") UUID subjectId, @PathVariable("page") Page page, @RequestParam("userId") UUID userId) { + // empty } - public void testMethodWithStringCourtId( - @PathVariable("courtId") String courtId, + public void testMethodWithStringSubjectId( + @PathVariable("subjectType") SubjectType subjectType, + @PathVariable("subjectId") String subjectId, @PathVariable("page") Page page, @RequestParam("userId") UUID userId) { + // empty } public void testMethodWithStringPage( - @PathVariable("courtId") UUID courtId, + @PathVariable("subjectType") SubjectType subjectType, + @PathVariable("subjectId") UUID subjectId, @PathVariable("page") String page, @RequestParam("userId") UUID userId) { + // empty } - public void testMethodWithoutCourtId( + public void testMethodWithoutSubject( @PathVariable("page") Page page, @RequestParam("userId") UUID userId) { + // empty } public void testMethodWithoutPage( - @PathVariable("courtId") UUID courtId, + @PathVariable("subjectType") SubjectType subjectType, + @PathVariable("subjectId") UUID subjectId, @RequestParam("userId") UUID userId) { + // empty } public void testMethodWithoutUserId( - @PathVariable("courtId") UUID courtId, + @PathVariable("subjectType") SubjectType subjectType, + @PathVariable("subjectId") UUID subjectId, @PathVariable("page") Page page) { + // empty } } } - diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java index ab915c3f..2234a22c 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/audit/AuditableCourtEntityListenerTest.java @@ -10,7 +10,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.entities.Court; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import java.util.UUID; @@ -120,7 +120,7 @@ void generateDiffsShouldReturnChangesWhenValuesDiffer() throws Exception { Audit audit = auditCaptor.getValue(); assertEquals(USER_ID, audit.getUserId()); assertEquals(COURT_ID, audit.getSubjectId()); - assertEquals(AuditSubjectType.COURT, audit.getSubjectType()); + assertEquals(SubjectType.COURT, audit.getSubjectType()); assertEquals(1, audit.getActionDataDiff().size()); assertEquals("name", audit.getActionDataDiff().getFirst().field()); } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java index 341d2741..2ec3a364 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/AuditControllerTest.java @@ -10,7 +10,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.entities.Court; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException; @@ -123,18 +123,18 @@ void getFilteredAndPaginatedAuditsThrowsInvalidParameterCombinationForBothSubjec @Test void getSubjectNameAndIdMapReturns200() { - Map> subjectMap = Map.of( - AuditSubjectType.COURT, List.of( + Map> subjectMap = Map.of( + SubjectType.COURT, List.of( new NameAndId("Exeter Law Courts", UUID.randomUUID()) ), - AuditSubjectType.SERVICE_CENTRE, List.of( + SubjectType.SERVICE_CENTRE, List.of( new NameAndId("Traffic Enforcement Centre", UUID.randomUUID()) ) ); when(auditService.getSubjectNameAndIdMap()).thenReturn(subjectMap); - ResponseEntity>> response = auditController.getSubjectNameAndIdMap(); + ResponseEntity>> response = auditController.getSubjectNameAndIdMap(); assertEquals(HttpStatus.OK, response.getStatusCode(), RESPONSE_STATUS_MISMATCH); assertEquals(subjectMap, response.getBody(), RESPONSE_BODY_MISMATCH); @@ -183,7 +183,7 @@ private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) .subjectId(COURT_ID) - .subjectType(AuditSubjectType.COURT) + .subjectType(SubjectType.COURT) .actionEntity(Court.class.getSimpleName()) .actionDataDiff(List.of( new Change("isOpen", Boolean.FALSE, Boolean.TRUE), diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockControllerTest.java similarity index 57% rename from src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockControllerTest.java rename to src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockControllerTest.java index afc03189..27f6251c 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/CourtLockControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/LockControllerTest.java @@ -7,9 +7,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; -import uk.gov.hmcts.reform.fact.data.api.services.CourtLockService; +import uk.gov.hmcts.reform.fact.data.api.services.LockService; import java.util.List; import java.util.Optional; @@ -20,28 +21,29 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class CourtLockControllerTest { +class LockControllerTest { private static final UUID COURT_ID = UUID.randomUUID(); private static final UUID USER_ID = UUID.randomUUID(); private static final String INVALID_UUID = "abcde"; - private static final Page TEST_PAGE = Page.COURT; + private static final Page TEST_PAGE = Page.GENERAL; private static final String RESPONSE_STATUS_MESSAGE = "Response status does not match"; private static final String RESPONSE_BODY_MESSAGE = "Response body does not match"; @Mock - private CourtLockService courtLockService; + private LockService lockService; @InjectMocks - private CourtLockController courtLockController; + private LockController lockController; @Test void getCourtLocksReturns200() { - List locks = List.of(new CourtLock()); - when(courtLockService.getLocksByCourtId(COURT_ID)).thenReturn(locks); + List locks = List.of(new Lock()); + when(lockService.getAllSubjectLocks(SubjectType.COURT, COURT_ID)).thenReturn(locks); - ResponseEntity> response = courtLockController.getCourtLocks(COURT_ID.toString()); + ResponseEntity> response = + lockController.getSubjectLocks(SubjectType.COURT, COURT_ID.toString()); assertThat(response.getStatusCode()).as(RESPONSE_STATUS_MESSAGE).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).as(RESPONSE_BODY_MESSAGE).isEqualTo(locks); @@ -51,16 +53,17 @@ void getCourtLocksReturns200() { void getCourtLocksThrowsIllegalArgumentExceptionForInvalidUUID() { assertThrows( IllegalArgumentException.class, () -> - courtLockController.getCourtLocks(INVALID_UUID) + lockController.getSubjectLocks(SubjectType.COURT, INVALID_UUID) ); } @Test void getCourtLockStatusReturns200() { - Optional lock = Optional.of(new CourtLock()); - when(courtLockService.getPageLock(COURT_ID, TEST_PAGE)).thenReturn(lock); + Lock lock = new Lock(); + when(lockService.getPageLock(SubjectType.COURT, COURT_ID, TEST_PAGE)).thenReturn(Optional.of(lock)); - ResponseEntity> response = courtLockController.getCourtLockStatus( + ResponseEntity response = lockController.getSubjectLockStatus( + SubjectType.COURT, COURT_ID.toString(), TEST_PAGE ); @@ -73,16 +76,17 @@ void getCourtLockStatusReturns200() { void getCourtLockStatusThrowsIllegalArgumentExceptionForInvalidUUID() { assertThrows( IllegalArgumentException.class, () -> - courtLockController.getCourtLockStatus(INVALID_UUID, TEST_PAGE) + lockController.getSubjectLockStatus(SubjectType.COURT, INVALID_UUID, TEST_PAGE) ); } @Test - void createCourtLockReturns201() { - CourtLock lock = new CourtLock(); - when(courtLockService.createOrUpdateLock(COURT_ID, TEST_PAGE, USER_ID)).thenReturn(lock); + void createOrUpdateCourtLockReturns201() { + Lock lock = new Lock(); + when(lockService.createOrUpdateLock(SubjectType.COURT, COURT_ID, TEST_PAGE, USER_ID)).thenReturn(lock); - ResponseEntity response = courtLockController.createOrUpdateCourtLock( + ResponseEntity response = lockController.createOrUpdateSubjectLock( + SubjectType.COURT, COURT_ID.toString(), TEST_PAGE, USER_ID @@ -96,28 +100,14 @@ void createCourtLockReturns201() { void createCourtLockThrowsIllegalArgumentExceptionForInvalidUUID() { assertThrows( IllegalArgumentException.class, () -> - courtLockController.createOrUpdateCourtLock(INVALID_UUID, TEST_PAGE, USER_ID) + lockController.createOrUpdateSubjectLock(SubjectType.COURT, INVALID_UUID, TEST_PAGE, USER_ID) ); } - @Test - void updateCourtLockReturns201() { - CourtLock lock = new CourtLock(); - when(courtLockService.createOrUpdateLock(COURT_ID, TEST_PAGE, USER_ID)).thenReturn(lock); - - ResponseEntity response = courtLockController.createOrUpdateCourtLock( - COURT_ID.toString(), - TEST_PAGE, - USER_ID - ); - - assertThat(response.getStatusCode()).as(RESPONSE_STATUS_MESSAGE).isEqualTo(HttpStatus.CREATED); - assertThat(response.getBody()).as(RESPONSE_BODY_MESSAGE).isEqualTo(lock); - } - @Test void deleteCourtLockReturns204() { - ResponseEntity response = courtLockController.deleteCourtLock(COURT_ID.toString(), TEST_PAGE); + ResponseEntity response = + lockController.deleteSubjectLock(SubjectType.COURT, COURT_ID.toString(), TEST_PAGE); assertThat(response.getStatusCode()).as(RESPONSE_STATUS_MESSAGE).isEqualTo(HttpStatus.NO_CONTENT); } @@ -126,7 +116,7 @@ void deleteCourtLockReturns204() { void deleteCourtLockThrowsIllegalArgumentExceptionForInvalidUUID() { assertThrows( IllegalArgumentException.class, () -> - courtLockController.deleteCourtLock(INVALID_UUID, TEST_PAGE) + lockController.deleteSubjectLock(SubjectType.COURT, INVALID_UUID, TEST_PAGE) ); } } diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserControllerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserControllerTest.java index 8d477605..5ce05207 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/controllers/UserControllerTest.java @@ -12,7 +12,7 @@ import uk.gov.hmcts.reform.fact.data.api.entities.Court; import uk.gov.hmcts.reform.fact.data.api.entities.User; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.services.CourtLockService; +import uk.gov.hmcts.reform.fact.data.api.services.LockService; import uk.gov.hmcts.reform.fact.data.api.services.UserService; import java.util.List; @@ -39,7 +39,7 @@ class UserControllerTest { private UserService userService; @Mock - private CourtLockService courtLockService; + private LockService lockService; @InjectMocks private UserController userController; @@ -163,7 +163,7 @@ void clearUserLocksReturns204() { @Test void clearUserLocksThrowsNotFoundExceptionWhenUserNotFound() { doThrow(new NotFoundException("User not found")) - .when(courtLockService).clearUserLocks(UNKNOWN_USER_ID); + .when(lockService).clearUserLocks(UNKNOWN_USER_ID); assertThrows( NotFoundException.class, () -> diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java index 688908ca..ea522ee6 100644 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AuditServiceTest.java @@ -12,7 +12,7 @@ import uk.gov.hmcts.reform.fact.data.api.config.properties.AuditConfigurationProperties; import uk.gov.hmcts.reform.fact.data.api.entities.Audit; import uk.gov.hmcts.reform.fact.data.api.entities.Court; -import uk.gov.hmcts.reform.fact.data.api.entities.types.AuditSubjectType; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; import uk.gov.hmcts.reform.fact.data.api.entities.types.Change; import uk.gov.hmcts.reform.fact.data.api.entities.types.NameAndId; import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException; @@ -134,7 +134,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfter() { when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( eq(COURT_ID), - eq(AuditSubjectType.COURT), + eq(SubjectType.COURT), eq(fromDateTime), any(Pageable.class) ) @@ -154,7 +154,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfter() { assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtAfter( - eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) + eq(COURT_ID), eq(SubjectType.COURT), eq(fromDateTime), any(Pageable.class) ); } @@ -163,7 +163,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetween() { when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetween( eq(COURT_ID), - eq(AuditSubjectType.COURT), + eq(SubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) @@ -184,7 +184,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetween() { assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtBetween( - eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) + eq(COURT_ID), eq(SubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) ); } @@ -252,7 +252,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfterAndEma when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( eq(COURT_ID), - eq(AuditSubjectType.COURT), + eq(SubjectType.COURT), eq(fromDateTime), eq(EMAIL), any(Pageable.class) @@ -273,7 +273,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtAfterAndEma assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtAfterAndEmailAddressLike( - eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), eq(EMAIL), any(Pageable.class) + eq(COURT_ID), eq(SubjectType.COURT), eq(fromDateTime), eq(EMAIL), any(Pageable.class) ); } @@ -282,7 +282,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetweenAndE when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( eq(COURT_ID), - eq(AuditSubjectType.COURT), + eq(SubjectType.COURT), eq(fromDateTime), eq(toDateTime), eq(EMAIL), @@ -304,7 +304,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByCourtIdAndCreatedAtBetweenAndE assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtBetweenAndEmailAddressLike( - eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), eq(toDateTime), eq(EMAIL), any(Pageable.class) + eq(COURT_ID), eq(SubjectType.COURT), eq(fromDateTime), eq(toDateTime), eq(EMAIL), any(Pageable.class) ); } @@ -328,7 +328,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByServiceCentreIdAndCreatedAtAft when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( eq(SERVICE_CENTRE_ID), - eq(AuditSubjectType.SERVICE_CENTRE), + eq(SubjectType.SERVICE_CENTRE), eq(fromDateTime), any(Pageable.class) ) @@ -348,7 +348,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindByServiceCentreIdAndCreatedAtAft assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtAfter( - eq(SERVICE_CENTRE_ID), eq(AuditSubjectType.SERVICE_CENTRE), eq(fromDateTime), any(Pageable.class) + eq(SERVICE_CENTRE_ID), eq(SubjectType.SERVICE_CENTRE), eq(fromDateTime), any(Pageable.class) ); } @@ -429,11 +429,11 @@ void getSubjectNameAndIdMapShouldAllSubjectData() { when(courtService.getAllCourtNameAndIds()).thenReturn(courts); when(serviceCentreService.getAllServiceCentreNameAndIds()).thenReturn(serviceCentres); - Map> result = auditService.getSubjectNameAndIdMap(); + Map> result = auditService.getSubjectNameAndIdMap(); - assertThat(result).hasSize(2).containsKeys(AuditSubjectType.COURT, AuditSubjectType.SERVICE_CENTRE); - assertThat(result.get(AuditSubjectType.COURT)).containsExactlyElementsOf(courts); - assertThat(result.get(AuditSubjectType.SERVICE_CENTRE)).containsExactlyElementsOf(serviceCentres); + assertThat(result).hasSize(2).containsKeys(SubjectType.COURT, SubjectType.SERVICE_CENTRE); + assertThat(result.get(SubjectType.COURT)).containsExactlyElementsOf(courts); + assertThat(result.get(SubjectType.SERVICE_CENTRE)).containsExactlyElementsOf(serviceCentres); verify(courtService).getAllCourtNameAndIds(); verify(serviceCentreService).getAllServiceCentreNameAndIds(); } @@ -443,11 +443,11 @@ void getSubjectNameAndIdMapShouldReturnEmptyListsWhenNoSubjectsExist() { when(courtService.getAllCourtNameAndIds()).thenReturn(List.of()); when(serviceCentreService.getAllServiceCentreNameAndIds()).thenReturn(List.of()); - Map> result = auditService.getSubjectNameAndIdMap(); + Map> result = auditService.getSubjectNameAndIdMap(); - assertThat(result).containsKeys(AuditSubjectType.COURT, AuditSubjectType.SERVICE_CENTRE); - assertThat(result.get(AuditSubjectType.COURT)).isEmpty(); - assertThat(result.get(AuditSubjectType.SERVICE_CENTRE)).isEmpty(); + assertThat(result).containsKeys(SubjectType.COURT, SubjectType.SERVICE_CENTRE); + assertThat(result.get(SubjectType.COURT)).isEmpty(); + assertThat(result.get(SubjectType.SERVICE_CENTRE)).isEmpty(); verify(courtService).getAllCourtNameAndIds(); verify(serviceCentreService).getAllServiceCentreNameAndIds(); } @@ -480,7 +480,7 @@ void getAuditByIdShouldThrowNotFoundExceptionWhenRecordDoesNotExist() { @Test void getFilteredAndPaginatedAuditsShouldCallFindBySubjectTypeAndCreatedAtAfterWhenOnlySubjectTypeProvided() { when(auditRepository.findBySubjectTypeAndCreatedAtAfter( - eq(AuditSubjectType.COURT), + eq(SubjectType.COURT), eq(fromDateTime), any(Pageable.class) ) @@ -492,7 +492,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindBySubjectTypeAndCreatedAtAfterWh PAGE_SIZE, fromDate, null, - AuditSubjectType.COURT, + SubjectType.COURT, null, null, null @@ -500,14 +500,14 @@ void getFilteredAndPaginatedAuditsShouldCallFindBySubjectTypeAndCreatedAtAfterWh assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectTypeAndCreatedAtAfter( - eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) + eq(SubjectType.COURT), eq(fromDateTime), any(Pageable.class) ); } @Test void getFilteredAndPaginatedAuditsShouldCallFindBySubjectTypeAndCreatedAtBetweenWhenSubjectTypeAndToDateProvided() { when(auditRepository.findBySubjectTypeAndCreatedAtBetween( - eq(AuditSubjectType.COURT), + eq(SubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) @@ -520,7 +520,7 @@ void getFilteredAndPaginatedAuditsShouldCallFindBySubjectTypeAndCreatedAtBetween PAGE_SIZE, fromDate, toDate, - AuditSubjectType.COURT, + SubjectType.COURT, null, null, null @@ -528,14 +528,14 @@ void getFilteredAndPaginatedAuditsShouldCallFindBySubjectTypeAndCreatedAtBetween assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectTypeAndCreatedAtBetween( - eq(AuditSubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) + eq(SubjectType.COURT), eq(fromDateTime), eq(toDateTime), any(Pageable.class) ); } @Test void shouldCallFindBySubjectTypeAndCreatedAtAfterAndEmailAddressLikeWhenSubjectTypeAndEmailProvided() { when(auditRepository.findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( - eq(AuditSubjectType.SERVICE_CENTRE), + eq(SubjectType.SERVICE_CENTRE), eq(fromDateTime), eq(EMAIL), any(Pageable.class) @@ -548,7 +548,7 @@ void shouldCallFindBySubjectTypeAndCreatedAtAfterAndEmailAddressLikeWhenSubjectT PAGE_SIZE, fromDate, null, - AuditSubjectType.SERVICE_CENTRE, + SubjectType.SERVICE_CENTRE, null, null, EMAIL @@ -556,14 +556,14 @@ void shouldCallFindBySubjectTypeAndCreatedAtAfterAndEmailAddressLikeWhenSubjectT assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectTypeAndCreatedAtAfterAndEmailAddressLike( - eq(AuditSubjectType.SERVICE_CENTRE), eq(fromDateTime), eq(EMAIL), any(Pageable.class) + eq(SubjectType.SERVICE_CENTRE), eq(fromDateTime), eq(EMAIL), any(Pageable.class) ); } @Test void shouldCallFindBySubjectTypeAndCreatedAtBetweenAndEmailAddressLikeWhenAllFiltersProvided() { when(auditRepository.findBySubjectTypeAndCreatedAtBetweenAndEmailAddressLike( - eq(AuditSubjectType.SERVICE_CENTRE), + eq(SubjectType.SERVICE_CENTRE), eq(fromDateTime), eq(toDateTime), eq(EMAIL), @@ -577,7 +577,7 @@ void shouldCallFindBySubjectTypeAndCreatedAtBetweenAndEmailAddressLikeWhenAllFil PAGE_SIZE, fromDate, toDate, - AuditSubjectType.SERVICE_CENTRE, + SubjectType.SERVICE_CENTRE, null, null, EMAIL @@ -585,14 +585,14 @@ void shouldCallFindBySubjectTypeAndCreatedAtBetweenAndEmailAddressLikeWhenAllFil assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectTypeAndCreatedAtBetweenAndEmailAddressLike( - eq(AuditSubjectType.SERVICE_CENTRE), eq(fromDateTime), eq(toDateTime), eq(EMAIL), any(Pageable.class) + eq(SubjectType.SERVICE_CENTRE), eq(fromDateTime), eq(toDateTime), eq(EMAIL), any(Pageable.class) ); } @Test void getFilteredAndPaginatedAuditsShouldIgnoreBlankEmailForSubjectTypeAndUseSimpleSubjectTypeQuery() { when(auditRepository.findBySubjectTypeAndCreatedAtAfter( - eq(AuditSubjectType.COURT), + eq(SubjectType.COURT), eq(fromDateTime), any(Pageable.class) ) @@ -604,7 +604,7 @@ void getFilteredAndPaginatedAuditsShouldIgnoreBlankEmailForSubjectTypeAndUseSimp PAGE_SIZE, fromDate, null, - AuditSubjectType.COURT, + SubjectType.COURT, null, null, "" @@ -616,7 +616,7 @@ void getFilteredAndPaginatedAuditsShouldIgnoreBlankEmailForSubjectTypeAndUseSimp PAGE_SIZE, fromDate, null, - AuditSubjectType.COURT, + SubjectType.COURT, null, null, " " @@ -625,7 +625,7 @@ void getFilteredAndPaginatedAuditsShouldIgnoreBlankEmailForSubjectTypeAndUseSimp assertThat(firstResult.getContent()).hasSize(1); assertThat(secondResult.getContent()).hasSize(1); verify(auditRepository, times(2)).findBySubjectTypeAndCreatedAtAfter( - eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) + eq(SubjectType.COURT), eq(fromDateTime), any(Pageable.class) ); } @@ -633,7 +633,7 @@ void getFilteredAndPaginatedAuditsShouldIgnoreBlankEmailForSubjectTypeAndUseSimp void getFilteredAndPaginatedAuditsShouldPrioritiseCourtIdOverSubjectTypeWhenBothProvided() { when(auditRepository.findBySubjectIdAndSubjectTypeAndCreatedAtAfter( eq(COURT_ID), - eq(AuditSubjectType.COURT), + eq(SubjectType.COURT), eq(fromDateTime), any(Pageable.class) ) @@ -645,7 +645,7 @@ void getFilteredAndPaginatedAuditsShouldPrioritiseCourtIdOverSubjectTypeWhenBoth PAGE_SIZE, fromDate, null, - AuditSubjectType.SERVICE_CENTRE, + SubjectType.SERVICE_CENTRE, COURT_ID.toString(), null, null @@ -653,7 +653,7 @@ void getFilteredAndPaginatedAuditsShouldPrioritiseCourtIdOverSubjectTypeWhenBoth assertThat(result.getContent()).hasSize(1); verify(auditRepository).findBySubjectIdAndSubjectTypeAndCreatedAtAfter( - eq(COURT_ID), eq(AuditSubjectType.COURT), eq(fromDateTime), any(Pageable.class) + eq(COURT_ID), eq(SubjectType.COURT), eq(fromDateTime), any(Pageable.class) ); } @@ -661,7 +661,7 @@ private Audit createAudit() { return Audit.builder() .id(AUDIT_ID) .subjectId(COURT_ID) - .subjectType(AuditSubjectType.COURT) + .subjectType(SubjectType.COURT) .actionEntity(Court.class.getSimpleName()) .actionDataDiff(List.of( new Change("isOpen", Boolean.FALSE, Boolean.TRUE), diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtLockServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtLockServiceTest.java deleted file mode 100644 index 1ac8ec1e..00000000 --- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtLockServiceTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package uk.gov.hmcts.reform.fact.data.api.services; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import uk.gov.hmcts.reform.fact.data.api.entities.User; -import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; -import uk.gov.hmcts.reform.fact.data.api.entities.Court; -import uk.gov.hmcts.reform.fact.data.api.entities.CourtLock; -import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; -import uk.gov.hmcts.reform.fact.data.api.repositories.CourtLockRepository; - -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class CourtLockServiceTest { - - @Mock - private CourtLockRepository courtLockRepository; - - @Mock - private CourtService courtService; - - @Mock - private UserService userService; - - @InjectMocks - private CourtLockService courtLockService; - - private UUID userId; - private User user; - private UUID courtId; - private Court court; - private CourtLock lock; - - @BeforeEach - void setup() { - courtId = UUID.randomUUID(); - court = new Court(); - court.setId(courtId); - court.setName("Test Court"); - - userId = UUID.randomUUID(); - user = new User(); - user.setId(userId); - user.setEmail("email@justice.gov.uk"); - - lock = new CourtLock(); - lock.setCourtId(courtId); - lock.setPage(Page.COURT); - lock.setLockAcquired(ZonedDateTime.now()); - } - - @Test - void clearUserLocksShouldDeleteAllLocksForUser() { - when(userService.getUserById(userId)).thenReturn(user); - courtLockService.clearUserLocks(userId); - - verify(courtLockRepository).deleteAllByUserId(userId); - } - - @Test - void clearUserLocksShouldThrowNotFoundExceptionWhenUserDoesNotExist() { - when(userService.getUserById(userId)).thenThrow(new NotFoundException("User not found")); - - assertThrows(NotFoundException.class, () -> courtLockService.clearUserLocks(userId)); - } - - @Test - void deleteLocksByUserIdShouldHandleNoLocksForUser() { - UUID newUserId = UUID.randomUUID(); - User newUser = new User(); - newUser.setId(newUserId); - when(userService.getUserById(newUserId)).thenReturn(newUser); - courtLockService.clearUserLocks(newUserId); - verify(courtLockRepository).deleteAllByUserId(newUserId); - } - - @Test - void getLocksByCourtIdShouldGetLocksByCourtId() { - when(courtService.getCourtById(courtId)).thenReturn(court); - when(courtLockRepository.findAllByCourtId(courtId)).thenReturn(List.of(lock)); - List result = courtLockService.getLocksByCourtId(courtId); - assertEquals(1, result.size()); - verify(courtService).getCourtById(courtId); - verify(courtLockRepository).findAllByCourtId(courtId); - } - - @Test - void getPageLockShouldGetPageLock() { - Page page = Page.COURT; - when(courtService.getCourtById(courtId)).thenReturn(court); - when(courtLockRepository.findByCourtIdAndPage(courtId, page)).thenReturn(Optional.of(lock)); - Optional result = courtLockService.getPageLock(courtId, page); - assertTrue(result.isPresent()); - verify(courtService).getCourtById(courtId); - verify(courtLockRepository).findByCourtIdAndPage(courtId, page); - } - - @Test - void createLockShouldCreateLock() { - when(courtService.getCourtById(courtId)).thenReturn(court); - when(userService.getUserById(userId)).thenReturn(user); - when(courtLockRepository.save(any(CourtLock.class))).thenReturn(lock); - CourtLock result = courtLockService.createOrUpdateLock(courtId, Page.COURT, userId); - assertNotNull(result); - verify(courtLockRepository).save(any(CourtLock.class)); - } - - @Test - void createOrUpdateLockShouldUpdateExistingLock() { - Page page = Page.COURT; - when(courtService.getCourtById(courtId)).thenReturn(court); - when(userService.getUserById(userId)).thenReturn(user); - when(courtLockRepository.findByCourtIdAndPage(courtId, page)).thenReturn(Optional.of(lock)); - when(courtLockRepository.save(any(CourtLock.class))).thenReturn(lock); - CourtLock result = courtLockService.createOrUpdateLock(courtId, page, userId); - assertNotNull(result); - verify(courtService).getCourtById(courtId); - verify(courtLockRepository).findByCourtIdAndPage(courtId, page); - verify(courtLockRepository).save(any(CourtLock.class)); - } - - @Test - void deleteLockShouldDeleteLock() { - Page page = Page.COURT; - when(courtService.getCourtById(courtId)).thenReturn(court); - courtLockService.deleteLock(courtId, page); - verify(courtService).getCourtById(courtId); - verify(courtLockRepository).deleteByCourtIdAndPage(courtId, page); - } -} - diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/LockServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/LockServiceTest.java new file mode 100644 index 00000000..afc67fc5 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/LockServiceTest.java @@ -0,0 +1,251 @@ +package uk.gov.hmcts.reform.fact.data.api.services; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import uk.gov.hmcts.reform.fact.data.api.entities.ServiceCentre; +import uk.gov.hmcts.reform.fact.data.api.entities.User; +import uk.gov.hmcts.reform.fact.data.api.entities.types.SubjectType; +import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException; +import uk.gov.hmcts.reform.fact.data.api.entities.Court; +import uk.gov.hmcts.reform.fact.data.api.entities.Lock; +import uk.gov.hmcts.reform.fact.data.api.entities.types.Page; +import uk.gov.hmcts.reform.fact.data.api.repositories.LockRepository; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LockServiceTest { + + @Mock + private LockRepository lockRepository; + + @Mock + private CourtService courtService; + + @Mock + private ServiceCentreService serviceCentreService; + + @Mock + private UserService userService; + + @InjectMocks + private LockService lockService; + + private UUID userId; + private User user; + private UUID courtId; + private Court court; + private Lock lock; + private UUID serviceCentreId; + private ServiceCentre serviceCentre; + + @BeforeEach + void setup() { + courtId = UUID.randomUUID(); + court = new Court(); + court.setId(courtId); + court.setName("Test Court"); + + userId = UUID.randomUUID(); + user = new User(); + user.setId(userId); + user.setEmail("email@justice.gov.uk"); + + lock = new Lock(); + lock.setId(UUID.randomUUID()); + lock.setSubjectType(SubjectType.COURT); + lock.setSubjectId(courtId); + lock.setPage(Page.GENERAL); + lock.setLockAcquired(ZonedDateTime.now()); + + serviceCentreId = UUID.randomUUID(); + serviceCentre = new ServiceCentre(); + serviceCentre.setId(serviceCentreId); + serviceCentre.setName("Test Service Centre"); + + // needed for deleteExpiredLocks test + ReflectionTestUtils.setField(lockService, "lockTimeoutMinutes", 30L); + } + + @Test + void clearUserLocksShouldDeleteAllLocksForUser() { + when(userService.getUserById(userId)).thenReturn(user); + lockService.clearUserLocks(userId); + + verify(lockRepository).deleteAllByUserId(userId); + } + + @Test + void clearUserLocksShouldThrowNotFoundExceptionWhenUserDoesNotExist() { + when(userService.getUserById(userId)).thenThrow(new NotFoundException("User not found")); + + assertThrows(NotFoundException.class, () -> lockService.clearUserLocks(userId)); + } + + @Test + void deleteLocksByUserIdShouldHandleNoLocksForUser() { + UUID newUserId = UUID.randomUUID(); + User newUser = new User(); + newUser.setId(newUserId); + when(userService.getUserById(newUserId)).thenReturn(newUser); + lockService.clearUserLocks(newUserId); + verify(lockRepository).deleteAllByUserId(newUserId); + } + + @Test + void getLocksByCourtIdShouldGetLocksByCourtId() { + when(courtService.getCourtById(courtId)).thenReturn(court); + when(lockRepository.findAllBySubjectTypeAndSubjectId(SubjectType.COURT, courtId)) + .thenReturn(List.of(lock)); + List result = lockService.getAllSubjectLocks(SubjectType.COURT, courtId); + assertEquals(1, result.size()); + verify(courtService).getCourtById(courtId); + verify(lockRepository).findAllBySubjectTypeAndSubjectId(SubjectType.COURT, courtId); + } + + @Test + void getPageLockShouldGetPageLock() { + Page page = Page.GENERAL; + when(courtService.getCourtById(courtId)).thenReturn(court); + when(lockRepository.findBySubjectTypeAndSubjectIdAndPage(SubjectType.COURT, courtId, page)) + .thenReturn(Optional.of(lock)); + Optional result = lockService.getPageLock(SubjectType.COURT, courtId, page); + assertTrue(result.isPresent()); + verify(courtService).getCourtById(courtId); + verify(lockRepository).findBySubjectTypeAndSubjectIdAndPage(SubjectType.COURT, courtId, page); + } + + @Test + void createLockShouldCreateLock() { + UUID lockId = UUID.randomUUID(); + when(courtService.getCourtById(courtId)).thenReturn(court); + when(userService.getUserById(userId)).thenReturn(user); + when(lockRepository.upsertLockAndDeleteOtherUserLocks( + any(), eq(SubjectType.COURT.name()), eq(courtId), + eq(Page.GENERAL.name()), eq(userId), any(ZonedDateTime.class) + )).thenReturn(lockId); + + Lock result = lockService.createOrUpdateLock(SubjectType.COURT, courtId, Page.GENERAL, userId); + + assertNotNull(result); + assertEquals(lockId, result.getId()); + verify(lockRepository).upsertLockAndDeleteOtherUserLocks( + any(), eq(SubjectType.COURT.name()), eq(courtId), + eq(Page.GENERAL.name()), eq(userId), any(ZonedDateTime.class) + ); + } + + @Test + void createOrUpdateLockShouldUpdateExistingLock() { + Page page = Page.GENERAL; + UUID lockId = UUID.randomUUID(); + when(courtService.getCourtById(courtId)).thenReturn(court); + when(userService.getUserById(userId)).thenReturn(user); + when(lockRepository.upsertLockAndDeleteOtherUserLocks( + any(), eq(SubjectType.COURT.name()), eq(courtId), eq(page.name()), eq(userId), any(ZonedDateTime.class) + )).thenReturn(lockId); + + Lock result = lockService.createOrUpdateLock(SubjectType.COURT, courtId, page, userId); + + assertNotNull(result); + assertEquals(lockId, result.getId()); + verify(courtService).getCourtById(courtId); + verify(lockRepository).upsertLockAndDeleteOtherUserLocks( + any(), eq(SubjectType.COURT.name()), eq(courtId), eq(page.name()), eq(userId), any(ZonedDateTime.class) + ); + } + + @Test + void deleteLockShouldDeleteLock() { + Page page = Page.GENERAL; + when(courtService.getCourtById(courtId)).thenReturn(court); + lockService.deleteLock(SubjectType.COURT, courtId, page); + verify(courtService).getCourtById(courtId); + verify(lockRepository).deleteBySubjectTypeAndSubjectIdAndPage(SubjectType.COURT.name(), courtId, page.name()); + } + + @Test + void getLocksByServiceCentreIdShouldGetLocksByServiceCentreId() { + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(lockRepository.findAllBySubjectTypeAndSubjectId(SubjectType.SERVICE_CENTRE, serviceCentreId)) + .thenReturn(List.of(lock)); + + List result = lockService.getAllSubjectLocks(SubjectType.SERVICE_CENTRE, serviceCentreId); + + assertEquals(1, result.size()); + verify(serviceCentreService).getServiceCentreById(serviceCentreId); + verify(lockRepository).findAllBySubjectTypeAndSubjectId(SubjectType.SERVICE_CENTRE, serviceCentreId); + } + + @Test + void getPageLockShouldGetPageLockForServiceCentre() { + Page page = Page.GENERAL; + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(lockRepository.findBySubjectTypeAndSubjectIdAndPage(SubjectType.SERVICE_CENTRE, serviceCentreId, page)) + .thenReturn(Optional.of(lock)); + + Optional result = lockService.getPageLock(SubjectType.SERVICE_CENTRE, serviceCentreId, page); + + assertTrue(result.isPresent()); + verify(serviceCentreService).getServiceCentreById(serviceCentreId); + verify(lockRepository).findBySubjectTypeAndSubjectIdAndPage(SubjectType.SERVICE_CENTRE, serviceCentreId, page); + } + + @Test + void createLockShouldCreateLockForServiceCentre() { + UUID lockId = UUID.randomUUID(); + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + when(userService.getUserById(userId)).thenReturn(user); + when(lockRepository.upsertLockAndDeleteOtherUserLocks( + any(), eq(SubjectType.SERVICE_CENTRE.name()), eq(serviceCentreId), eq(Page.GENERAL.name()), eq(userId), + any(ZonedDateTime.class) + )).thenReturn(lockId); + + Lock result = lockService.createOrUpdateLock(SubjectType.SERVICE_CENTRE, serviceCentreId, Page.GENERAL, userId); + + assertNotNull(result); + assertEquals(lockId, result.getId()); + verify(serviceCentreService).getServiceCentreById(serviceCentreId); + verify(lockRepository).upsertLockAndDeleteOtherUserLocks( + any(), eq(SubjectType.SERVICE_CENTRE.name()), eq(serviceCentreId), eq(Page.GENERAL.name()), eq(userId), + any(ZonedDateTime.class) + ); + } + + @Test + void deleteLockShouldDeleteLockForServiceCentre() { + Page page = Page.GENERAL; + when(serviceCentreService.getServiceCentreById(serviceCentreId)).thenReturn(serviceCentre); + + lockService.deleteLock(SubjectType.SERVICE_CENTRE, serviceCentreId, page); + + verify(serviceCentreService).getServiceCentreById(serviceCentreId); + verify(lockRepository).deleteBySubjectTypeAndSubjectIdAndPage( + SubjectType.SERVICE_CENTRE.name(), serviceCentreId, page.name()); + } + + @Test + void deleteExpiredLocksShouldDeleteLocksBeforeConfiguredTimeout() { + lockService.deleteExpiredLocks(); + + verify(lockRepository).deleteByLockAcquiredBefore(any(ZonedDateTime.class)); + } +}