Skip to content

Commit

Permalink
[ESWE-1181] Employer Creation; MN API client, registrar; revise tests (
Browse files Browse the repository at this point in the history
…#20)

- implemented `MNJobBoardApiClient`, with integration tests
- implemented `EmployerRegistrar`
- rename test objects' file to `EmployerObjects`
- `EmployerService` is incomplete
- revise tests of JobsBoardApiClient
  • Loading branch information
rickchoijd authored Jan 28, 2025
1 parent c02d16f commit e7373a2
Show file tree
Hide file tree
Showing 23 changed files with 303 additions and 13 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ testing {
implementation("org.apache.commons:commons-compress:1.27.1")
}
implementation("org.testcontainers:localstack")
implementation("org.jetbrains.kotlin:kotlin-test-junit5")
}

targets {
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ services:
# TODO: Remove this URL and replace with outgoing service URLs
- EXAMPLE_URL=http://hmpps-jobs-board-integration-api:8080
- SPRING_PROFILES_ACTIVE=dev,local
- API_BASE_URL_JOBSBOARD=https://jobs-board-api-dev.hmpps.service.justice.gov.uk
- API_BASE_URL_MNJOBBOARD=https://testservices.sequation.net/sequation-job-api
- MN_JOBBOARD_API_TOKEN=<OVERRIDE_THIS>

hmpps-auth:
image: quay.io/hmpps/hmpps-auth:latest
Expand Down
1 change: 1 addition & 0 deletions helm_deploy/hmpps-jobs-board-integration-api/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ generic-service:
EXAMPLE_API_CLIENT_SECRET: "TEMPLATE_KOTLIN_API_CLIENT_SECRET"
API_CLIENT_ID: "SYSTEM_CLIENT_ID"
API_CLIENT_SECRET: "SYSTEM_CLIENT_SECRET"
MN_JOBBOARD_API_TOKEN: "MN_JOBBOARD_API_TOKEN"
application-insights:
APPLICATIONINSIGHTS_CONNECTION_STRING: "APPLICATIONINSIGHTS_CONNECTION_STRING"

Expand Down
1 change: 1 addition & 0 deletions helm_deploy/values-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ generic-service:
# TODO: This should be replaced by a call to a different service, or removed
EXAMPLE_API_URL: "https://jobs-board-integration-api-dev.hmpps.service.justice.gov.uk"
API_BASE_URL_JOBSBOARD: "https://jobs-board-api-dev.hmpps.service.justice.gov.uk"
API_BASE_URL_MNJOBBOARD: "https://testservices.sequation.net/sequation-job-api"

serviceAccountName: education-skills-work-employment-dev

Expand Down
1 change: 1 addition & 0 deletions helm_deploy/values-preprod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ generic-service:
# TODO: This should be replaced by a call to a different service, or removed
EXAMPLE_API_URL: "https://jobs-board-integration-api-preprod.hmpps.service.justice.gov.uk"
API_BASE_URL_JOBSBOARD: "https://jobs-board-api-preprod.hmpps.service.justice.gov.uk"
API_BASE_URL_MNJOBBOARD: "https://preprodservices.sequation.net/sequation-job-api" # URL to be confirmed

serviceAccountName: education-skills-work-employment-preprod

Expand Down
1 change: 1 addition & 0 deletions helm_deploy/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ generic-service:
# TODO: This should be replaced by a call to a different service, or removed
EXAMPLE_API_URL: "https://jobs-board-integration-api.hmpps.service.justice.gov.uk"
API_BASE_URL_JOBSBOARD: "https://jobs-board-api.hmpps.service.justice.gov.uk"
API_BASE_URL_MNJOBBOARD: "https://liveservices.sequation.net/sequation-job-api" # URL to be confirmed

serviceAccountName: education-skills-work-employment-prod

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain

object EmployerMother {
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.MNEmployer

object EmployerObjects {
val tesco = Employer(
id = "89de6c84-3372-4546-bbc1-9d1dc9ceb354",
name = "Tesco",
Expand Down Expand Up @@ -41,3 +43,22 @@ object EmployerMother {
status = "SILVER",
)
}

internal fun Employer.mnEmployer() = MNEmployer(
employerName = name,
employerBio = description,
sectorId = sectorIdMap[sector]!!,
partnerId = statusPartnerIdMap[status]!!,
)

private val sectorIdMap = mapOf(
"CONSTRUCTION" to 6,
"LOGISTICS" to 8,
"RETAIL" to 7,
)

private val statusPartnerIdMap = mapOf(
"SILVER" to 3,
"GOLD" to 2,
"KEY_PARTNER" to 1,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock
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.integration.wiremock.MNJobBoardApiExtension
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.application.DefaultTimeProvider
import uk.gov.justice.hmpps.test.kotlin.auth.JwtAuthorisationHelper
import java.time.Instant
Expand All @@ -31,6 +32,7 @@ import java.util.*
HmppsAuthApiExtension::class,
ExampleApiExtension::class,
JobsBoardApiExtension::class,
MNJobBoardApiExtension::class,
MockitoExtension::class,
)
@SpringBootTest(webEnvironment = RANDOM_PORT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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.employers.domain.EmployerObjects.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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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.EmployerObjects.sainsburys
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.mnEmployer
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.IntegrationTestBase
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremock.MNJobBoardApiExtension.Companion.mnJobBoardApi
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.CreatEmployerRequest
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.MNEmployer
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.MNJobBoardApiWebClient
import kotlin.test.assertFailsWith

class MNJobBoardApiWebClientShould : IntegrationTestBase() {

@Autowired
private lateinit var apiWebClient: MNJobBoardApiWebClient

@Nested
@DisplayName("MN JobBoard `POST` /employers")
inner class EmployersPostEndpoint {
private val employer = sainsburys
private val mnEmployer: MNEmployer get() = employer.copy(createdAt = timeProvider.nowAsInstant()).mnEmployer()

@Test
fun `create employer, with valid details`() {
val expectedEmployer = mnEmployer.copy(id = 1L)
mnJobBoardApi.stubCreateEmployer(mnEmployer, expectedEmployer.id!!)

val actualEmployer = CreatEmployerRequest.from(mnEmployer).let { apiWebClient.createEmployer(it) }

assertThat(actualEmployer).isEqualTo(expectedEmployer)
}

@Test
fun `receive unauthorised error, if API access token is invalid`() {
mnJobBoardApi.stubCreateEmployerUnauthorised()

val exception = assertFailsWith<Exception> {
CreatEmployerRequest.from(mnEmployer).let { apiWebClient.createEmployer(it) }
}

with(exception) {
assertThat(message).contains("Fail to create employer") // reactive throw (ReactiveException)
with(cause!!) {
assertThat(message).contains("Fail to create employer") // actual throw (Exception)
with(cause!!) {
assertThat(message).contains("401 Unauthorized") // cause (401) (WebClientResponseException$Unauthorized)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.wiremoc

import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock.aResponse
import com.github.tomakehurst.wiremock.client.WireMock.containing
import com.github.tomakehurst.wiremock.client.WireMock.get
import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching
import org.junit.jupiter.api.extension.AfterAllCallback
Expand All @@ -16,6 +17,7 @@ class JobsBoardApiMockServer : WireMockServer(8092) {
fun stubRetrieveEmployer(employer: Employer) {
stubFor(
get(urlPathMatching(retrieveEmployerPathRegex))
.withHeader("Authorization", containing("Bearer"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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.matching
import com.github.tomakehurst.wiremock.client.WireMock.post
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.shared.infrastructure.MNEmployer
import java.util.concurrent.atomic.AtomicLong

private const val EMPLOYERS_ENDPOINT = "/employers"

class MNJobBoardApiMockServer(
private val nextId: AtomicLong = AtomicLong(1),
) : WireMockServer(8093) {
fun stubCreateEmployer(mnEmployer: MNEmployer) = stubCreateEmployer(mnEmployer, nextId.getAndIncrement())
fun stubCreateEmployer(mnEmployer: MNEmployer, newId: Long) {
val employerCreated = mnEmployer.copy(id = newId)
stubFor(
post(EMPLOYERS_ENDPOINT)
.withHeader("Authorization", matching("^Bearer .+\$"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
.withBody(employerCreated.response()),
),
)
}

fun stubCreateEmployerUnauthorised() {
stubFor(
post(EMPLOYERS_ENDPOINT)
.withHeader("Authorization", matching("^Bearer .+\$"))
.willReturn(
aResponse()
.withStatus(401),
),
)
}
}

class MNJobBoardApiExtension : BeforeAllCallback, AfterAllCallback, BeforeEachCallback {
companion object {
@JvmField
val mnJobBoardApi = MNJobBoardApiMockServer()
}

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

private fun MNEmployer.response() = """
{
"message": {
"successCode": "J2047",
"successMessage": "Successfully added employer",
"httpStatusCode": 201
},
"responseObject": {
"id": $id,
"employerName": "$employerName",
"employerBio": "$employerBio",
"sectorId": $sectorId,
"partnerId": $partnerId,
"imgName": ${imgName?.let { "\"$it\"" }},
"path": ${path?.let { "\"$it\"" }}
}
}
""".trimIndent()
6 changes: 5 additions & 1 deletion src/integrationTest/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ hmpps.sqs:
api:
base.url:
jobsboard: "http://localhost:8092"
mnjobboard: "http://localhost:8093"
client:
id: "api-client"
secret: "api-client-secret"

integration:
enabled: true
enabled: true

mn.jobboard:
api.token: "mn-api-bearer-token"
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ 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 @@ -32,7 +31,18 @@ class WebClientConfiguration(
builder.authorisedWebClient(authorizedClientManager, registrationId = "example-api", url = exampleApiBaseUri, timeout)

@ConditionalOnIntegrationEnabled
@Bean("jobsBoardWebClient")
fun jobsBoardApiWebClient(authorizedClientManager: OAuth2AuthorizedClientManager, builder: WebClient.Builder): WebClient =
builder.authorisedWebClient(authorizedClientManager, registrationId = "hmpps-jobs-board-api", url = jobsboardApiBaseUri, timeout)
@Bean
fun jobsBoardWebClient(
authorizedClientManager: OAuth2AuthorizedClientManager,
builder: WebClient.Builder,
@Value("\${api.base.url.jobsboard}") jobsboardApiBaseUri: String,
): WebClient = builder.authorisedWebClient(authorizedClientManager, registrationId = "hmpps-jobs-board-api", url = jobsboardApiBaseUri, timeout)

@ConditionalOnIntegrationEnabled
@Bean
fun mnJobBoardWebClient(
builder: WebClient.Builder,
@Value("\${api.base.url.mnjobboard}") mnjobboardApiBaseUri: String,
@Value("\${mn.jobboard.api.token}") mnJobBoardToken: String,
): WebClient = builder.defaultHeader("Authorization", "Bearer $mnJobBoardToken").baseUrl(mnjobboardApiBaseUri).build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.Emp

@ConditionalOnIntegrationEnabled
@Service
class EmployerRegistrar {
class EmployerRegistrar(
private val employerService: EmployerService,
) {
fun registerCreation(employer: Employer) {
// TODO implement employer registration to MN job-board API
throw NotImplementedError("Employer-Creation's registration is not yet implemented!")
try {
val mnEmployer = employerService.run { create(convert(employer)) }
assert(mnEmployer.id != null) { "MN Employer ID is missing! employerId=${employer.id}, employerName=${employer.name}" }
employerService.createIdMapping(mnEmployer.id!!, employer.id)
} catch (ex: Exception) {
"Fail to register employer-creation; employerId=${employer.id}, employerName=${employer.name}".let { message ->
throw Exception(message, ex)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,37 @@ import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.config.ConditionalOnIntegrationEnabled
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.Employer
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.domain.JobsBoardApiClient
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.domain.MNJobBoardApiClient
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.CreatEmployerRequest
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.MNEmployer

@ConditionalOnIntegrationEnabled
@Service
class EmployerService(
private val jobsBoardApiClient: JobsBoardApiClient,
private val mnJobBoardApiClient: MNJobBoardApiClient,
) {
fun retrieveById(id: String): Employer? = jobsBoardApiClient.getEmployer(id)

fun create(mnEmployer: MNEmployer): MNEmployer {
val request = CreatEmployerRequest.from(mnEmployer)
// TODO cater optional fields for employerStatus = KEY_PARTNER (sectorId==2)
return mnJobBoardApiClient.createEmployer(request)
}

fun convert(employer: Employer) = employer.run {
MNEmployer(
employerName = name,
employerBio = description,
// FIXME translate from employer.sector
sectorId = 1,
// FIXME translate from employer.status
partnerId = 1,
)
}

fun createIdMapping(mnId: Long, employerId: String) {
// TODO persist ID mapping
throw NotImplementedError("ID Mapping is not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.domain

import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.CreatEmployerRequest
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.shared.infrastructure.CreatEmployerResponse

interface MNJobBoardApiClient {
fun createEmployer(request: CreatEmployerRequest): CreatEmployerResponse
}
Loading

0 comments on commit e7373a2

Please sign in to comment.