Skip to content

Commit

Permalink
[ESWE-1181] ID mappings for Employer, Reference Data (#23)
Browse files Browse the repository at this point in the history
- (dynamic) ID mappings for Employer
- (static) ID mappings for Reference Data (used by Employer)
- relevant integration tests (JPA repository)
- Flyway migrations for new table and materialised view
  • Loading branch information
rickchoijd authored Jan 29, 2025
1 parent a84c921 commit 5168863
Show file tree
Hide file tree
Showing 14 changed files with 546 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration

import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
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.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.http.HttpHeaders
Expand Down Expand Up @@ -37,6 +40,8 @@ import java.util.*
MockitoExtension::class,
)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureTestDatabase(replace = NONE)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ActiveProfiles("test")
abstract class IntegrationTestBase {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.config

import org.mockito.Mockito.mock
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Primary
import org.springframework.context.annotation.Profile
import org.springframework.data.auditing.DateTimeProvider
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@TestConfiguration
@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider")
@ComponentScan(basePackages = ["uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.shared.infrastructure"])
@Profile("test", "test-repo")
class TestJpaConfig {
@Primary
@Bean
fun dateTimeProvider(): DateTimeProvider {
return mock(DateTimeProvider::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.employers.infrastructure

import org.assertj.core.api.Assertions.assertThat
import org.hibernate.exception.ConstraintViolationException
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.dao.DataIntegrityViolationException
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.EmployerExternalId
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.EmployerObjects.sainsburys
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.EmployerObjects.tesco
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.shared.infrastructure.RepositoryTestCase
import kotlin.test.assertFailsWith

class EmployerExternalIdKeyRepositoryShould : RepositoryTestCase() {

@Test
fun `return empty list, when nothing has been created yet`() {
employerExternalIdRepository.findAll().let {
assertThat(it).isEmpty()
}
}

@Test
fun `return nothing, for any employer ID`() {
val employerId = randomId()
val actual = employerExternalIdRepository.findByKeyId(employerId)
assertThat(actual).isNull()
}

@Test
fun `return nothing, for any employer external ID`() {
val externalId = randomExtId()

val actual = employerExternalIdRepository.findByKeyExternalId(externalId)
assertThat(actual).isNull()
}

@Nested
@DisplayName("Given an employer has been created")
inner class GivenAnEmployer {
private val employer = sainsburys
private val expectedExtId = randomExtId()
private val externalId = EmployerExternalId(employer.id, expectedExtId)

@Nested
@DisplayName("And no external ID mapping has been created, for the given employer")
inner class AndNoExternalIdMapping {
@Test
fun `return nothing, for given employer ID`() {
val actual = employerExternalIdRepository.findByKeyId(employer.id)
assertThat(actual).isNull()
}

@Test
fun `create external ID mapping`() {
val savedExternalId = employerExternalIdRepository.save(externalId)

assertThat(savedExternalId.key.id).isEqualTo(employer.id)
assertThat(savedExternalId.key.externalId).isEqualTo(expectedExtId)
assertThat(savedExternalId).isEqualTo(externalId)
}

@Test
fun `record timestamps, when creating external ID mapping`() {
val savedExternalId = employerExternalIdRepository.save(externalId)
with(savedExternalId) {
assertThat(this.createdAt).isEqualTo(currentTime)
assertThat(this.lastModifiedAt).isEqualTo(currentTime)
}
}
}

@Nested
@DisplayName("And external ID mapping has been created, for the given employer")
inner class AndExternalIdMappingCreated {

@BeforeEach
internal fun setUp() {
employerExternalIdRepository.saveAndFlush(externalId)
}

@Test
fun `return employer external ID mapping, for given employer ID`() {
val actual = employerExternalIdRepository.findByKeyId(employer.id)
assertThat(actual).isNotNull.isEqualTo(externalId)
}

@Test
fun `return employer external ID mapping, for given employer external ID`() {
val actual = employerExternalIdRepository.findByKeyExternalId(expectedExtId)
assertThat(actual).isNotNull.isEqualTo(externalId)
}

@Test
fun `throw error, when creating with duplicate ID`() {
val newExternalId = EmployerExternalId(employer.id, randomExtId())

val exception = assertFailsWith<DataIntegrityViolationException> {
employerExternalIdRepository.saveAndFlush(newExternalId)
}

assertThat(exception.cause).isInstanceOfAny(ConstraintViolationException::class.java)
exception.cause!!.message!!.let {
assertThat(it)
.contains("ERROR: duplicate key value violates unique constraint")
.contains(employer.id)
}
}
}
}

@Nested
@DisplayName("Given two employers have been created")
inner class GivenTwoEmployers {
private val employerA = sainsburys
private val externalIdOfEmployerA = randomExtId()
private val employerB = tesco

private val employerExternalIdOfA = EmployerExternalId(employerA.id, externalIdOfEmployerA)

@BeforeEach
internal fun setUp() {
employerExternalIdRepository.saveAndFlush(employerExternalIdOfA)
}

@Test
fun `return nothing, for the employer without external ID mapped`() {
val actual = employerExternalIdRepository.findByKeyId(employerB.id)
assertThat(actual).isNull()
}

@Test
fun `throw error, when creating with duplicate external ID`() {
val newExternalId = EmployerExternalId(employerB.id, externalIdOfEmployerA)

val exception = assertFailsWith<DataIntegrityViolationException> {
employerExternalIdRepository.saveAndFlush(newExternalId)
}

assertThat(exception.cause).isInstanceOfAny(ConstraintViolationException::class.java)
exception.cause!!.message!!.let {
assertThat(it)
.contains("ERROR: duplicate key value violates unique constraint")
.contains(externalIdOfEmployerA.toString())
}
}

@Test
fun `create external ID mapping, without any duplicate ID or external ID`() {
val newExtId = randomExtId()
val newExternalId = EmployerExternalId(employerB.id, newExtId)

val savedExternalId = employerExternalIdRepository.saveAndFlush(newExternalId)

assertThat(savedExternalId.key.id).isEqualTo(employerB.id)
assertThat(savedExternalId.key.externalId).isEqualTo(newExtId)
assertThat(savedExternalId).isEqualTo(newExternalId)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.refdata.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 uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.shared.infrastructure.RepositoryTestCase
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.refdata.domain.RefDataMappingKey
import kotlin.test.assertTrue

class RefDataMappingRepositoryShould : RepositoryTestCase() {
@Test
fun `return non empty list of reference data mapping(s)`() {
val refDataMappings = this.refDataMappingRepository.findAll()
assertThat(refDataMappings).isNotEmpty
}

@Test
fun `return nothing for undefined reference data`() {
val unknownRefKey = RefDataMappingKey()
val refDataMapping = this.refDataMappingRepository.findById(unknownRefKey)

assertTrue(refDataMapping.isEmpty, "Nothing should be found!")
}

@Test
fun `return correct mapping, given reference data and data value`() {
val refData = "employer_status"
val dataValue = "GOLD"
val expectedExtId = 2L

val mapping = refDataMappingRepository.findByDataRefDataAndDataValue(refData, dataValue)

assertThat(mapping).hasSize(1)
assertThat(mapping.first().data.externalId).isEqualTo(expectedExtId)
}

@Test
fun `return correct mapping, given reference data and data external ID`() {
val refData = "employer_sector"
val dataExternalId = 6L
val expectedDataValue = "CONSTRUCTION"

val mapping = refDataMappingRepository.findByDataRefDataAndDataExternalId(refData, dataExternalId)
assertThat(mapping).hasSize(1)
assertThat(mapping.first().data.value).isEqualTo(expectedDataValue)
}

@Nested
@DisplayName("Given reference data mappings for employer")
inner class GivenRefDataMappingsForEmployer {
@Test
fun `return mappings of Employer Status`() = assertRefDataMappingsHasSize("employer_status", 3)

@Test
fun `return mappings of Employer Sector`() = assertRefDataMappingsHasSize("employer_sector", 19)
}

private fun assertRefDataMappingsHasSize(refData: String, expectedSize: Int) {
val refDataMappings = this.refDataMappingRepository.findByDataRefData(refData)
assertThat(refDataMappings).hasSize(expectedSize)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.shared.infrastructure

import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
import org.mockito.kotlin.whenever
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.data.auditing.DateTimeProvider
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.employers.domain.EmployerExternalIdRepository
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.config.TestJpaConfig
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.integration.testcontainers.PostgresContainer
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.refdata.domain.RefDataMapping
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.refdata.domain.RefDataMappingKey
import uk.gov.justice.digital.hmpps.jobsboardintegrationapi.refdata.domain.RefDataMappingRepository
import java.security.SecureRandom
import java.time.Instant
import java.util.*

@DataJpaTest
@Import(TestJpaConfig::class)
@AutoConfigureTestDatabase(replace = NONE)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ActiveProfiles("test-repo")
abstract class RepositoryTestCase {
@Autowired
protected lateinit var dateTimeProvider: DateTimeProvider

@Autowired
protected lateinit var refDataMappingRepository: RefDataMappingRepository

@Autowired
protected lateinit var employerExternalIdRepository: EmployerExternalIdRepository

@Autowired
private lateinit var refDataMappingTestOnlyRepository: RefDataMappingTestOnlyRepository

protected final val defaultCurrentTime: Instant = Instant.parse("2025-01-01T00:00:00.00Z")

protected val currentTime: Instant get() = defaultCurrentTime

companion object {
private val postgresContainer = PostgresContainer.repositoryContainer

@JvmStatic
@DynamicPropertySource
fun configureTestContainers(registry: DynamicPropertyRegistry) {
postgresContainer?.run {
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl)
registry.add("spring.datasource.username", postgresContainer::getUsername)
registry.add("spring.datasource.password", postgresContainer::getPassword)
}
}
}

@BeforeAll
fun setUpClass() {
refDataMappingTestOnlyRepository.saveAll(refDataMappingsForTests)
}

@BeforeEach
fun setUp() {
employerExternalIdRepository.deleteAll()

whenever(dateTimeProvider.now).thenAnswer { Optional.of(currentTime) }
}

protected fun randomId() = UUID.randomUUID().toString()
protected fun randomExtId() = SecureRandom().nextLong()

private val refDataMappingsForTests: List<RefDataMapping>
get() = mapOf(
"employer_status" to mapOf(
"KEY_PARTNER" to 1L,
"GOLD" to 2,
"SILVER" to 3,
),
"employer_sector" to mapOf(
"ADMIN_SUPPORT" to 14L,
"AGRICULTURE" to 1,
"ARTS_ENTERTAINMENT" to 18,
"CONSTRUCTION" to 6,
"EDUCATION" to 16,
"ENERGY" to 4,
"FINANCE" to 11,
"HEALTH_SOCIAL" to 17,
"HOSPITALITY_CATERING" to 9,
"LOGISTICS" to 8,
"MANUFACTURING" to 3,
"MINING" to 2,
"OTHER" to 19,
"PROFESSIONALS_SCIENTISTS_TECHNICIANS" to 13,
"PROPERTY" to 12,
"PUBLIC_ADMIN_DEFENCE" to 15,
"WASTE_MANAGEMENT" to 5,
"RETAIL" to 7,
"TECHNOLOGY" to 10,
),
).map { (refData, mapping) -> mapping.map { (value, externalId) -> RefDataMapping(refData, value, externalId) } }
.flatten()
}

@Repository
internal interface RefDataMappingTestOnlyRepository : JpaRepository<RefDataMapping, RefDataMappingKey>
Loading

0 comments on commit 5168863

Please sign in to comment.