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; MN API client, registrar; revise tests #20

Merged
merged 1 commit into from
Jan 28, 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 @@ -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"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

map secret value to env var, for MN APi client's authorisation

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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actual URL is TBD


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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actual URL is TBD


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,
)
}
Comment on lines +25 to +34
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is incomplete.
ID mapping will be implemented next.


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
Loading