Skip to content

Commit

Permalink
[ESWE-1181] Employer Creation; JB API client, retriever
Browse files Browse the repository at this point in the history
- implemented `JobsBoardApiClient`, with unit tests and integration tests
- implemented `EmployerRetriever`, with integration tests
  • Loading branch information
rickchoijd committed Jan 21, 2025
1 parent 99e9851 commit dbde16f
Show file tree
Hide file tree
Showing 18 changed files with 405 additions and 11 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ testing {
exclude(group = "io.swagger.core.v3")
exclude(group = "io.swagger.parser.v3", module = "swagger-parser-safe-url-resolver")
}
implementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
implementation("org.springframework.boot:spring-boot-testcontainers")
implementation("org.testcontainers:localstack")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain

object EmployerMother {
val tesco = Employer(
id = "89de6c84-3372-4546-bbc1-9d1dc9ceb354",
name = "Tesco",
description = "Tesco plc is a British multinational groceries and general merchandise retailer headquartered in Welwyn Garden City, England. The company was founded by Jack Cohen in Hackney, London in 1919.",
sector = "RETAIL",
status = "SILVER",
)

val tescoLogistics = Employer(
id = "2c8032bf-e583-4ae9-bcec-968a1c4881f9",
name = "Tesco",
description = "This is another Tesco employer that provides logistic services.",
sector = "LOGISTICS",
status = "GOLD",
)

val sainsburys = Employer(
id = "f4fbdbf3-823c-4877-aafc-35a7fa74a15a",
name = "Sainsbury's",
description = "J Sainsbury plc, trading as Sainsbury's, is a British supermarket and the second-largest chain of supermarkets in the United Kingdom. Founded in 1869 by John James Sainsbury with a shop in Drury Lane, London, the company was the largest UK retailer of groceries for most of the 20th century.",
sector = "RETAIL",
status = "GOLD",
)

val amazon = Employer(
id = "bf392249-b360-4e3e-81a0-8497047987e8",
name = "Amazon",
description = "Amazon.com, Inc., doing business as Amazon, is an American multinational technology company, engaged in e-commerce, cloud computing, online advertising, digital streaming, and artificial intelligence.",
sector = "LOGISTICS",
status = "KEY_PARTNER",
)

val abcConstruction = Employer(
id = "182e9a24-6edb-48a6-a84f-b7061f004a97",
name = "ABC Construction",
description = "This is a description",
sector = "CONSTRUCTION",
status = "SILVER",
)
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration

import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.whenever
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.http.HttpHeaders
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean
import org.springframework.test.web.reactive.server.WebTestClient
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.testcontainers.LocalStackContainer
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.testcontainers.LocalStackContainer.setLocalStackProperties
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock.ExampleApiExtension
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock.ExampleApiExtension.Companion.exampleApi
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock.HmppsAuthApiExtension
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock.JobsBoardApiExtension
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.application.DefaultTimeProvider
import uk.gov.justice.hmpps.test.kotlin.auth.JwtAuthorisationHelper
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*

@ExtendWith(HmppsAuthApiExtension::class, ExampleApiExtension::class)
@ExtendWith(
HmppsAuthApiExtension::class,
ExampleApiExtension::class,
JobsBoardApiExtension::class,
MockitoExtension::class,
)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
abstract class IntegrationTestBase {
Expand All @@ -28,6 +43,13 @@ abstract class IntegrationTestBase {
@Autowired
protected lateinit var jwtAuthHelper: JwtAuthorisationHelper

@MockitoSpyBean
protected lateinit var timeProvider: DefaultTimeProvider

val defaultTimezoneId = ZoneId.of("Z")
val defaultCurrentTime: Instant = Instant.parse("2024-01-01T00:00:00Z")
val defaultCurrentTimeLocal: LocalDateTime get() = defaultCurrentTime.atZone(defaultTimezoneId).toLocalDateTime()

companion object {
private val localStackContainer by lazy { LocalStackContainer.instance }

Expand All @@ -38,6 +60,12 @@ abstract class IntegrationTestBase {
}
}

@BeforeEach
internal fun setUp() {
whenever(timeProvider.timezoneId).thenReturn(defaultTimezoneId)
whenever(timeProvider.now()).thenReturn(defaultCurrentTimeLocal)
}

internal fun setAuthorisation(
username: String? = "AUTH_ADM",
roles: List<String> = listOf(),
Expand All @@ -48,4 +76,6 @@ abstract class IntegrationTestBase {
hmppsAuth.stubHealthPing(status)
exampleApi.stubHealthPing(status)
}

protected fun randomUUID() = UUID.randomUUID().toString()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.shared.infrastructure

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.EmployerMother.sainsburys
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.IntegrationTestBase
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock.JobsBoardApiExtension.Companion.jobsBoardApi
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.JobsBoardApiWebClient

class JobsBoardApiWebClientShould : IntegrationTestBase() {

@Autowired
private lateinit var jobsBoardApiWebClient: JobsBoardApiWebClient

@Nested
@DisplayName("JobsBoard `GET` /employers")
inner class EmployersGetEndpoint {
@Test
fun `return employer details, given valid employer ID`() {
val employer = sainsburys.copy(createdAt = timeProvider.nowAsInstant())

hmppsAuth.stubGrantToken()
jobsBoardApi.stubRetrieveEmployer(employer)

val actualEmployer = jobsBoardApiWebClient.getEmployer(employer.id)

assertThat(actualEmployer).isEqualTo(employer)
}

@Test
fun `return nothing, given invalid employer ID`() {
val employerId = randomUUID()

hmppsAuth.stubGrantToken()
jobsBoardApi.stubRetrieveEmployerNotFound()

val actualEmployer = jobsBoardApiWebClient.getEmployer(employerId)

assertThat(actualEmployer).isNull()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock

import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock.aResponse
import com.github.tomakehurst.wiremock.client.WireMock.get
import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.Employer

class JobsBoardApiMockServer : WireMockServer(8092) {
private val retrieveEmployerPathRegex = "/employers/[a-zA-Z0-9\\-]*"

fun stubRetrieveEmployer(employer: Employer) {
stubFor(
get(urlPathMatching(retrieveEmployerPathRegex))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
.withBody(employer.response()),
),
)
}

fun stubRetrieveEmployerNotFound() {
stubFor(
get(urlPathMatching(retrieveEmployerPathRegex))
.willReturn(
aResponse()
.withStatus(404),
),
)
}
}

class JobsBoardApiExtension : BeforeAllCallback, AfterAllCallback, BeforeEachCallback {
companion object {
@JvmField
val jobsBoardApi = JobsBoardApiMockServer()
}

override fun beforeAll(context: ExtensionContext): Unit = jobsBoardApi.start()
override fun beforeEach(context: ExtensionContext): Unit = jobsBoardApi.resetAll()
override fun afterAll(context: ExtensionContext): Unit = jobsBoardApi.stop()
}

private fun Employer.response() = """
{
"id": "$id",
"name": "$name",
"description": "$description",
"sector": "$sector",
"status": "$status",
"createdAt": ${createdAt?.let { "\"$it\"" }}
}
""".trimIndent()
10 changes: 10 additions & 0 deletions src/integrationTest/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ hmpps.sqs:
queues:
integrationqueue:
queueName: hmpps_jobs_board_integration_queue

api:
base.url:
jobsboard: "http://localhost:8092"
client:
id: "api-client"
secret: "api-client-secret"

integration:
enabled: true
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import java.time.Duration
class WebClientConfiguration(
@Value("\${example-api.url}") val exampleApiBaseUri: String,
@Value("\${hmpps-auth.url}") val hmppsAuthBaseUri: String,
@Value("\${api.base.url.jobsboard}") val jobsboardApiBaseUri: String,
@Value("\${api.health-timeout:2s}") val healthTimeout: Duration,
@Value("\${api.timeout:20s}") val timeout: Duration,
) {
Expand All @@ -29,4 +30,8 @@ class WebClientConfiguration(
@Bean
fun exampleApiWebClient(authorizedClientManager: OAuth2AuthorizedClientManager, builder: WebClient.Builder): WebClient =
builder.authorisedWebClient(authorizedClientManager, registrationId = "example-api", url = exampleApiBaseUri, timeout)

@Bean("jobsBoardWebClient")
fun jobsBoardApiWebClient(authorizedClientManager: OAuth2AuthorizedClientManager, builder: WebClient.Builder): WebClient =
builder.authorisedWebClient(authorizedClientManager, registrationId = "hmpps-jobs-board-api", url = jobsboardApiBaseUri, timeout)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ class EmployerCreationMessageService(

override fun handleEvent(employerEvent: EmployerEvent) {
log.info("handle employer creation event; eventId={}", employerEvent.eventId)
retriever.retrieve(employerEvent.employerId).also { employer ->
registrar.registerCreation(employer)
try {
retriever.retrieve(employerEvent.employerId).also { employer ->
registrar.registerCreation(employer)
}
} catch (e: Exception) {
throw Exception("Error at employer creation event: eventId=${employerEvent.eventId}", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.Employer

@Service
class EmployerRetriever {
class EmployerRetriever(
private val employerService: EmployerService,
) {
fun retrieve(id: String): Employer {
// TODO implement employer retrieval from MJMA jobs board API
throw NotImplementedError("Employer's retrieval is not yet implemented!")
return employerService.retrieveById(id) ?: run {
throw IllegalArgumentException("Employer id=$id not found")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.application

import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.Employer
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.domain.JobsBoardApiClient

@Service
class EmployerService(
private val jobsBoardApiClient: JobsBoardApiClient,
) {
fun retrieveById(id: String): Employer? = jobsBoardApiClient.getEmployer(id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.domain

import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.Employer

interface JobsBoardApiClient {
fun getEmployer(id: String): Employer?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException
import reactor.core.publisher.Mono
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.Employer
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.domain.JobsBoardApiClient
import java.time.Instant

@Service
class JobsBoardApiWebClient(
@Qualifier("jobsBoardWebClient") private val jobsBoardWebClient: WebClient,
) : JobsBoardApiClient {

companion object {
val log: Logger = LoggerFactory.getLogger(this::class.java)
}

override fun getEmployer(id: String): Employer? {
log.debug("Getting employer details with id={}", id)
return jobsBoardWebClient
.get().uri("/employers/{id}", id).accept(APPLICATION_JSON).retrieve()
.bodyToMono(GetEmployerResponse::class.java)
.onErrorResume(WebClientResponseException.NotFound::class.java) {
log.debug("Employer not found. employerId={}", id)
Mono.empty()
}.block()?.employer()
}
}

data class GetEmployerResponse(
val id: String,
val name: String,
val description: String,
val sector: String,
val status: String,
val createdAt: String,
) {
companion object {
fun from(employer: Employer): GetEmployerResponse {
return GetEmployerResponse(
id = employer.id,
name = employer.name,
description = employer.description,
sector = employer.sector,
status = employer.status,
createdAt = employer.createdAt.toString(),
)
}
}

fun employer() = Employer(
id = id,
name = name,
description = description,
sector = sector,
status = status,
createdAt = Instant.parse(createdAt),
)
}
8 changes: 6 additions & 2 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ example-api:
id: "example-api-client"
secret: "example-api-client-secret"

api.integration:
enabled: false
api:
base.url:
jobsboard: "https://jobs-board-api-dev.hmpps.service.justice.gov.uk"

integration:
enabled: false
Loading

0 comments on commit dbde16f

Please sign in to comment.