Skip to content

Commit 74be18e

Browse files
PRC-439: Integration tests and handling conflicts
1 parent a5404b9 commit 74be18e

File tree

5 files changed

+345
-3
lines changed

5 files changed

+345
-3
lines changed

helm_deploy/hmpps-organisations-api/values.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ generic-service:
2323
SERVER_PORT: "8080"
2424
APPLICATIONINSIGHTS_CONFIGURATION_FILE: applicationinsights.json
2525
DB_SSL_MODE: "verify-full"
26+
FEATURE_EVENTS_SNS_ENABLED: true
27+
FEATURE_EVENT_ORGANISATIONS_API_ORGANISATION_CREATED: true
28+
FEATURE_EVENT_ORGANISATIONS_API_ORGANISATION_UPDATED: true
29+
FEATURE_EVENT_ORGANISATIONS_API_ORGANISATION_DELETED: true
2630

2731
# Pre-existing kubernetes secrets to load as environment variables in the deployment.
2832
# namespace_secrets:

src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/config/OrganisationsApiExceptionHandler.kt

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import jakarta.validation.ValidationException
66
import org.apache.commons.lang3.exception.ExceptionUtils
77
import org.slf4j.LoggerFactory
88
import org.springframework.http.HttpStatus.BAD_REQUEST
9+
import org.springframework.http.HttpStatus.CONFLICT
910
import org.springframework.http.HttpStatus.FORBIDDEN
1011
import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR
1112
import org.springframework.http.HttpStatus.NOT_FOUND
@@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice
1920
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException
2021
import org.springframework.web.servlet.resource.NoResourceFoundException
2122
import uk.gov.justice.digital.hmpps.organisationsapi.exception.InvalidReferenceCodeGroupException
23+
import uk.gov.justice.digital.hmpps.organisationsapi.service.sync.DuplicateOrganisationException
2224
import uk.gov.justice.hmpps.kotlin.common.ErrorResponse
2325
import java.time.format.DateTimeParseException
2426

@@ -127,6 +129,17 @@ class OrganisationsApiExceptionHandler {
127129
),
128130
)
129131

132+
@ExceptionHandler(DuplicateOrganisationException::class)
133+
fun handleDuplicatePersonException(e: DuplicateOrganisationException): ResponseEntity<ErrorResponse> = ResponseEntity
134+
.status(CONFLICT)
135+
.body(
136+
ErrorResponse(
137+
status = CONFLICT,
138+
userMessage = e.message,
139+
developerMessage = e.message,
140+
),
141+
)
142+
130143
@ExceptionHandler(MissingServletRequestParameterException::class)
131144
fun handleValidationException(e: MissingServletRequestParameterException): ResponseEntity<ErrorResponse> = ResponseEntity
132145
.status(BAD_REQUEST)

src/main/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/resource/sync/SyncOrganisationController.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrg
2727
import uk.gov.justice.digital.hmpps.organisationsapi.swagger.AuthApiResponses
2828
import uk.gov.justice.hmpps.kotlin.common.ErrorResponse
2929

30-
@Tag(name = "Sync & Migrate")
30+
@Tag(name = "Migration and synchronisation")
3131
@RestController
3232
@RequestMapping(value = ["sync"], produces = [MediaType.APPLICATION_JSON_VALUE])
3333
@AuthApiResponses
@@ -96,7 +96,7 @@ class SyncOrganisationController(val syncFacade: SyncFacade) {
9696
@PostMapping(path = ["/organisation"], produces = [MediaType.APPLICATION_JSON_VALUE])
9797
@ResponseBody
9898
@Operation(
99-
summary = "Creates a new organisation",
99+
summary = "Creates a new organisation with a specified ID",
100100
description = """
101101
Requires role: ROLE_ORGANISATIONS_MIGRATION.
102102
Used to create a new organisation.

src/test/kotlin/uk/gov/justice/digital/hmpps/organisationsapi/integration/StubOutboundEventsPublisher.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEven
88
import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEventsPublisher
99
import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundHMPPSDomainEvent
1010

11-
class StubOutboundEventsPublisher(private val receivedEvents: MutableList<OutboundHMPPSDomainEvent> = mutableListOf()) : OutboundEventsPublisher {
11+
class StubOutboundEventsPublisher(
12+
private val receivedEvents: MutableList<OutboundHMPPSDomainEvent> = mutableListOf(),
13+
) : OutboundEventsPublisher {
1214
companion object {
1315
private val logger = LoggerFactory.getLogger(this::class.java)
1416
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
package uk.gov.justice.digital.hmpps.organisationsapi.integration.resource.sync
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.jupiter.api.BeforeEach
5+
import org.junit.jupiter.api.Nested
6+
import org.junit.jupiter.api.Test
7+
import org.springframework.http.HttpStatus
8+
import org.springframework.http.MediaType
9+
import org.springframework.test.context.TestPropertySource
10+
import uk.gov.justice.digital.hmpps.organisationsapi.integration.PostgresIntegrationTestBase
11+
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncCreateOrganisationRequest
12+
import uk.gov.justice.digital.hmpps.organisationsapi.model.request.sync.SyncUpdateOrganisationRequest
13+
import uk.gov.justice.digital.hmpps.organisationsapi.model.response.sync.SyncOrganisationResponse
14+
import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OrganisationInfo
15+
import uk.gov.justice.digital.hmpps.organisationsapi.service.events.OutboundEvent
16+
import uk.gov.justice.digital.hmpps.organisationsapi.service.events.Source
17+
import uk.gov.justice.hmpps.kotlin.common.ErrorResponse
18+
import java.time.LocalDateTime
19+
20+
@TestPropertySource(properties = ["feature.events.sns.enabled=true"])
21+
class SyncOrganisationsIntegrationTest : PostgresIntegrationTestBase() {
22+
23+
@Nested
24+
inner class OrganisationSyncTests {
25+
26+
@BeforeEach
27+
fun resetEvents() {
28+
stubEvents.reset()
29+
}
30+
31+
@Test
32+
fun `Sync endpoints should return unauthorized if no token provided`() {
33+
webTestClient.get()
34+
.uri("/sync/organisation/1")
35+
.accept(MediaType.APPLICATION_JSON)
36+
.exchange()
37+
.expectStatus()
38+
.isUnauthorized
39+
40+
webTestClient.post()
41+
.uri("/sync/organisation")
42+
.accept(MediaType.APPLICATION_JSON)
43+
.contentType(MediaType.APPLICATION_JSON)
44+
.bodyValue(syncCreateOrganisationRequest(5000L))
45+
.exchange()
46+
.expectStatus()
47+
.isUnauthorized
48+
49+
webTestClient.put()
50+
.uri("/sync/organisation/1")
51+
.accept(MediaType.APPLICATION_JSON)
52+
.contentType(MediaType.APPLICATION_JSON)
53+
.bodyValue(syncUpdateOrganisationRequest(5000L))
54+
.exchange()
55+
.expectStatus()
56+
.isUnauthorized
57+
58+
webTestClient.delete()
59+
.uri("/sync/organisation/1")
60+
.accept(MediaType.APPLICATION_JSON)
61+
.exchange()
62+
.expectStatus()
63+
.isUnauthorized
64+
}
65+
66+
@Test
67+
fun `Sync endpoints should return forbidden without an authorised role on the token`() {
68+
webTestClient.get()
69+
.uri("/sync/organisation/1")
70+
.accept(MediaType.APPLICATION_JSON)
71+
.headers(setAuthorisation(roles = listOf("ROLE_WRONG")))
72+
.exchange()
73+
.expectStatus()
74+
.isForbidden
75+
76+
webTestClient.post()
77+
.uri("/sync/organisation")
78+
.accept(MediaType.APPLICATION_JSON)
79+
.contentType(MediaType.APPLICATION_JSON)
80+
.bodyValue(syncCreateOrganisationRequest(5000L))
81+
.headers(setAuthorisation(roles = listOf("ROLE_WRONG")))
82+
.exchange()
83+
.expectStatus()
84+
.isForbidden
85+
86+
webTestClient.put()
87+
.uri("/sync/organisation/1")
88+
.accept(MediaType.APPLICATION_JSON)
89+
.contentType(MediaType.APPLICATION_JSON)
90+
.bodyValue(syncUpdateOrganisationRequest(5000L))
91+
.headers(setAuthorisation(roles = listOf("ROLE_WRONG")))
92+
.exchange()
93+
.expectStatus()
94+
.isForbidden
95+
96+
webTestClient.delete()
97+
.uri("/sync/organisation/1")
98+
.accept(MediaType.APPLICATION_JSON)
99+
.headers(setAuthorisation(roles = listOf("ROLE_WRONG")))
100+
.exchange()
101+
.expectStatus()
102+
.isForbidden
103+
}
104+
105+
@Test
106+
fun `should create and then get an organisation by ID`() {
107+
val organisationCreated = createOrganisationWithFixedId(5001L)
108+
val organisation = getOrganisationById(organisationCreated.organisationId)
109+
110+
with(organisation) {
111+
assertThat(this.organisationId).isEqualTo(organisationId)
112+
assertThat(organisationName).isEqualTo("Organisation123")
113+
assertThat(programmeNumber).isEqualTo("PRG123")
114+
assertThat(vatNumber).isEqualTo("VAT123")
115+
assertThat(caseloadId).isEqualTo("HEI")
116+
assertThat(comments).isEqualTo("comment123")
117+
assertThat(active).isEqualTo(true)
118+
assertThat(deactivatedDate).isNull()
119+
assertThat(createdBy).isEqualTo("CREATOR")
120+
assertThat(createdTime).isAfter(LocalDateTime.now().minusMinutes(5))
121+
assertThat(updatedBy).isNull()
122+
assertThat(updatedTime).isNull()
123+
}
124+
125+
stubEvents.assertHasEvent(
126+
event = OutboundEvent.ORGANISATION_CREATED,
127+
additionalInfo = OrganisationInfo(organisation.organisationId, organisation.organisationId, Source.NOMIS),
128+
)
129+
}
130+
131+
@Test
132+
fun `should create a new organisation with fixed ID`() {
133+
val organisation = createOrganisationWithFixedId(5002L)
134+
with(organisation) {
135+
assertThat(this.organisationId).isEqualTo(5002L)
136+
assertThat(organisationName).isEqualTo("Organisation123")
137+
assertThat(programmeNumber).isEqualTo("PRG123")
138+
assertThat(vatNumber).isEqualTo("VAT123")
139+
assertThat(caseloadId).isEqualTo("HEI")
140+
assertThat(comments).isEqualTo("comment123")
141+
assertThat(active).isEqualTo(true)
142+
assertThat(deactivatedDate).isNull()
143+
assertThat(createdBy).isEqualTo("CREATOR")
144+
assertThat(createdTime).isAfter(LocalDateTime.now().minusMinutes(5))
145+
assertThat(updatedBy).isNull()
146+
assertThat(updatedTime).isNull()
147+
}
148+
149+
stubEvents.assertHasEvent(
150+
event = OutboundEvent.ORGANISATION_CREATED,
151+
additionalInfo = OrganisationInfo(organisation.organisationId, organisation.organisationId, Source.NOMIS),
152+
)
153+
}
154+
155+
@Test
156+
fun `should create and then update an organisation`() {
157+
val organisation = createOrganisationWithFixedId(5003L)
158+
with(organisation) {
159+
assertThat(this.organisationId).isEqualTo(5003L)
160+
assertThat(organisationName).isEqualTo("Organisation123")
161+
}
162+
163+
val updated = updateOrganisation(organisation.organisationId)
164+
with(updated) {
165+
assertThat(this.organisationId).isEqualTo(5003L)
166+
assertThat(organisationName).isEqualTo("Organisation321")
167+
assertThat(programmeNumber).isEqualTo("PRG321")
168+
assertThat(vatNumber).isEqualTo("VAT321")
169+
assertThat(comments).isEqualTo("comment321")
170+
assertThat(caseloadId).isEqualTo("AGI")
171+
assertThat(active).isEqualTo(true)
172+
assertThat(createdBy).isEqualTo("CREATOR")
173+
assertThat(createdTime).isAfter(LocalDateTime.now().minusMinutes(5))
174+
assertThat(updatedBy).isEqualTo("UPDATER")
175+
assertThat(updatedTime).isAfter(LocalDateTime.now().minusMinutes(5))
176+
}
177+
178+
stubEvents.assertHasEvent(
179+
event = OutboundEvent.ORGANISATION_CREATED,
180+
additionalInfo = OrganisationInfo(organisation.organisationId, organisation.organisationId, Source.NOMIS),
181+
)
182+
183+
stubEvents.assertHasEvent(
184+
event = OutboundEvent.ORGANISATION_UPDATED,
185+
additionalInfo = OrganisationInfo(updated.organisationId, updated.organisationId, Source.NOMIS),
186+
)
187+
}
188+
189+
@Test
190+
fun `should create and then delete an organisation`() {
191+
val organisation = createOrganisationWithFixedId(5004L)
192+
with(organisation) {
193+
assertThat(this.organisationId).isEqualTo(5004L)
194+
assertThat(organisationName).isEqualTo("Organisation123")
195+
}
196+
197+
webTestClient.delete()
198+
.uri("/sync/organisation/{organisationId}", organisation.organisationId)
199+
.accept(MediaType.APPLICATION_JSON)
200+
.headers(setAuthorisation(roles = listOf("ROLE_ORGANISATIONS_MIGRATION")))
201+
.exchange()
202+
.expectStatus()
203+
.isOk
204+
205+
webTestClient.get()
206+
.uri("/sync/organisations/{organisationId}", organisation.organisationId)
207+
.accept(MediaType.APPLICATION_JSON)
208+
.headers(setAuthorisation(roles = listOf("ROLE_ORGANISATIONS_MIGRATION")))
209+
.exchange()
210+
.expectStatus()
211+
.isNotFound
212+
213+
stubEvents.assertHasEvent(
214+
event = OutboundEvent.ORGANISATION_CREATED,
215+
additionalInfo = OrganisationInfo(organisation.organisationId, organisation.organisationId, Source.NOMIS),
216+
)
217+
218+
stubEvents.assertHasEvent(
219+
event = OutboundEvent.ORGANISATION_DELETED,
220+
additionalInfo = OrganisationInfo(organisation.organisationId, organisation.organisationId, Source.NOMIS),
221+
)
222+
}
223+
224+
@Test
225+
fun `should report a conflict when creating an organisation ID that already exists`() {
226+
val organisation = createOrganisationWithFixedId(5005L)
227+
with(organisation) {
228+
assertThat(this.organisationId).isEqualTo(5005L)
229+
assertThat(organisationName).isEqualTo("Organisation123")
230+
}
231+
232+
stubEvents.assertHasEvent(
233+
event = OutboundEvent.ORGANISATION_CREATED,
234+
additionalInfo = OrganisationInfo(organisation.organisationId, organisation.organisationId, Source.NOMIS),
235+
)
236+
237+
resetEvents()
238+
239+
val expectedError = webTestClient.post()
240+
.uri("/sync/organisation")
241+
.accept(MediaType.APPLICATION_JSON)
242+
.contentType(MediaType.APPLICATION_JSON)
243+
.headers(setAuthorisation(roles = listOf("ROLE_ORGANISATIONS_MIGRATION")))
244+
.bodyValue(syncCreateOrganisationRequest(organisation.organisationId))
245+
.exchange()
246+
.expectStatus()
247+
.is4xxClientError
248+
.expectHeader().contentType(MediaType.APPLICATION_JSON)
249+
.expectBody(ErrorResponse::class.java)
250+
.returnResult().responseBody!!
251+
252+
assertThat(expectedError.status).isEqualTo(HttpStatus.CONFLICT.value())
253+
assertThat(expectedError.userMessage).isEqualTo("Sync: Duplicate organisation ID received 5005")
254+
255+
stubEvents.assertHasNoEvents(OutboundEvent.ORGANISATION_CREATED)
256+
}
257+
258+
private fun syncUpdateOrganisationRequest(organisationId: Long) = SyncUpdateOrganisationRequest(
259+
organisationId = organisationId,
260+
organisationName = "Organisation321",
261+
programmeNumber = "PRG321",
262+
vatNumber = "VAT321",
263+
caseloadId = "AGI",
264+
comments = "comment321",
265+
active = true,
266+
updatedBy = "UPDATER",
267+
updatedTime = LocalDateTime.now(),
268+
)
269+
270+
private fun syncCreateOrganisationRequest(organisationId: Long) = SyncCreateOrganisationRequest(
271+
// Sync creates supply a fixed ID from NOMIS (i.e. the corporate ID)
272+
organisationId = organisationId,
273+
organisationName = "Organisation123",
274+
programmeNumber = "PRG123",
275+
vatNumber = "VAT123",
276+
caseloadId = "HEI",
277+
comments = "comment123",
278+
active = true,
279+
createdTime = LocalDateTime.now(),
280+
createdBy = "CREATOR",
281+
)
282+
283+
private fun createOrganisationWithFixedId(organisationId: Long) =
284+
webTestClient.post()
285+
.uri("/sync/organisation")
286+
.accept(MediaType.APPLICATION_JSON)
287+
.contentType(MediaType.APPLICATION_JSON)
288+
.headers(setAuthorisation(roles = listOf("ROLE_ORGANISATIONS_MIGRATION")))
289+
.bodyValue(syncCreateOrganisationRequest(organisationId))
290+
.exchange()
291+
.expectStatus()
292+
.isOk
293+
.expectHeader().contentType(MediaType.APPLICATION_JSON)
294+
.expectBody(SyncOrganisationResponse::class.java)
295+
.returnResult().responseBody!!
296+
297+
private fun getOrganisationById(organisationId: Long) =
298+
webTestClient.get()
299+
.uri("/sync/organisation/{organisationId}", organisationId)
300+
.accept(MediaType.APPLICATION_JSON)
301+
.headers(setAuthorisation(roles = listOf("ROLE_ORGANISATIONS_MIGRATION")))
302+
.exchange()
303+
.expectStatus()
304+
.isOk
305+
.expectHeader().contentType(MediaType.APPLICATION_JSON)
306+
.expectBody(SyncOrganisationResponse::class.java)
307+
.returnResult().responseBody!!
308+
309+
private fun updateOrganisation(organisationId: Long) =
310+
webTestClient.put()
311+
.uri("/sync/organisation/{organisationId}", organisationId)
312+
.accept(MediaType.APPLICATION_JSON)
313+
.contentType(MediaType.APPLICATION_JSON)
314+
.headers(setAuthorisation(roles = listOf("ROLE_ORGANISATIONS_MIGRATION")))
315+
.bodyValue(syncUpdateOrganisationRequest(organisationId))
316+
.exchange()
317+
.expectStatus()
318+
.isOk
319+
.expectHeader().contentType(MediaType.APPLICATION_JSON)
320+
.expectBody(SyncOrganisationResponse::class.java)
321+
.returnResult().responseBody!!
322+
}
323+
}

0 commit comments

Comments
 (0)