Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ESWE-1181] Employer Creation; JB API client, retriever #15

Merged
merged 2 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copied from jobs-board backend

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming and idea was taken from this pattern: https://martinfowler.com/bliki/ObjectMother.html
Thanks for asking @gbmojo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename it later

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/{id}")
inner class EmployersGetDetailsEndpoint {
@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
Loading