Skip to content
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 .mvn/maven.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-T 1C
16 changes: 14 additions & 2 deletions app/src/test/kotlin/finance/idem/TestcontainersConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ import org.testcontainers.utility.DockerImageName

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
companion object {
// JVM-wide singletons: every @SpringBootTest context that imports this
// configuration shares one Postgres + Redis pair instead of booting its
// own. Held here (not as context-managed lifecycle) so a context close
// never stops the containers; Ryuk reaps them at JVM exit.
private val postgres: PostgreSQLContainer<*> =
PostgreSQLContainer(DockerImageName.parse("postgres:16")).also { it.start() }

private val redis: GenericContainer<*> =
GenericContainer(DockerImageName.parse("redis:7")).withExposedPorts(6379).also { it.start() }
}

@Bean
@ServiceConnection
fun postgresContainer(): PostgreSQLContainer<*> = PostgreSQLContainer(DockerImageName.parse("postgres:16"))
fun postgresContainer(): PostgreSQLContainer<*> = postgres

@Bean
@ServiceConnection(name = "redis")
fun redisContainer(): GenericContainer<*> = GenericContainer(DockerImageName.parse("redis:7")).withExposedPorts(6379)
fun redisContainer(): GenericContainer<*> = redis
}
4 changes: 4 additions & 0 deletions application/src/test/resources/junit-platform.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Class-level parallel test execution — safe here: no Spring context, no containers.
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import finance.idem.core.TenantId
interface ApiKeyRepository {
fun save(apiKey: ApiKey): ApiKey

fun findByPrefix(prefix: String): ApiKey?
/**
* Returns every key sharing this prefix. The prefix (first 12 chars of the raw
* key) is not unique — its index is non-unique by design — so callers must
* disambiguate by bcrypt-matching the raw key against each candidate's hash.
*/
fun findAllByPrefix(prefix: String): List<ApiKey>

fun findById(
id: ApiKeyId,
Expand Down
4 changes: 4 additions & 0 deletions core/src/test/resources/junit-platform.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Class-level parallel test execution — safe here: no Spring context, no containers.
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID

interface ApiKeyJpaRepository : JpaRepository<ApiKeyDataModel, UUID> {
fun findByPrefix(prefix: String): ApiKeyDataModel?
fun findAllByPrefix(prefix: String): List<ApiKeyDataModel>

fun findByIdAndTenantId(
id: UUID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ApiKeyRepositoryAdapter(
}

@Transactional(readOnly = true)
override fun findByPrefix(prefix: String): ApiKey? = jpaRepository.findByPrefix(prefix)?.toDomain()
override fun findAllByPrefix(prefix: String): List<ApiKey> = jpaRepository.findAllByPrefix(prefix).map { it.toDomain() }

@Transactional(readOnly = true)
override fun findById(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ class ApiKeyService(
deserializeFromCache(json)?.let { return it }
}

val apiKey = apiKeyRepository.findByPrefix(prefix) ?: return null
if (apiKey.isRevoked) return null
if (!passwordEncoder.matches(rawKey, apiKey.keyHash)) {
log.warn("Hash mismatch for prefix={}***", prefix.take(6))
return null
}
// The prefix is not unique (only ~65k values, non-unique index), so several
// keys can share it — bcrypt-match the raw key against each live candidate.
val candidates = apiKeyRepository.findAllByPrefix(prefix).filterNot { it.isRevoked }
val apiKey =
candidates.firstOrNull { passwordEncoder.matches(rawKey, it.keyHash) } ?: run {
if (candidates.isNotEmpty()) log.warn("Hash mismatch for prefix={}***", prefix.take(6))
return null
}

val validated = ValidatedApiKey(apiKey.tenantId, apiKey.scopes)
redisTemplate.opsForValue().set(cacheKey, serializeForCache(validated), cacheTtl)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package finance.idem.infrastructure

import finance.idem.infrastructure.service.PostgresTestContainers
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource

/**
* Points the subclass's Spring context at the module-wide singleton Postgres
* container instead of a per-class one, so the whole module boots one container
* (and runs Flyway once) rather than one per test class.
*
* Safe only for tests whose writes roll back (e.g. `@DataJpaTest`) or are scoped
* to a per-class random `TenantId`. Tests that commit rows a scheduler or another
* class could observe (poller/orchestrator/telemetry-ping) and tests that assert
* fresh-database state (FlywayMigrationTest) must keep their own container.
*/
@Suppress("UtilityClassWithPublicConstructor") // subclassed so Spring inherits the @DynamicPropertySource
abstract class SharedPostgresTestBase {
companion object {
@DynamicPropertySource
@JvmStatic
fun sharedPostgresProps(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", PostgresTestContainers.postgres::getJdbcUrl)
registry.add("spring.datasource.username", PostgresTestContainers.postgres::getUsername)
registry.add("spring.datasource.password", PostgresTestContainers.postgres::getPassword)
// Cached contexts each hold a live Hikari pool against the ONE shared
// container — at the default pool size (10) ~25 cached contexts exceed
// Postgres max_connections. Two connections suffice for slice tests.
registry.add("spring.datasource.hikari.maximum-pool-size") { "2" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,21 @@ import finance.idem.core.MonetaryAmount
import finance.idem.core.StablecoinToken
import finance.idem.core.TenantId
import finance.idem.core.monetary.OnChainEntry
import finance.idem.infrastructure.SharedPostgresTestBase
import finance.idem.infrastructure.persistence.PersistenceTestConfig
import jakarta.persistence.EntityManager
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@Import(ComplianceQueueRepositoryAdapter::class, PersistenceTestConfig::class)
class ComplianceQueueRepositoryAdapterTest {
companion object {
@Container
val postgres =
PostgreSQLContainer("postgres:16")
.withDatabaseName("idem_test")
.withUsername("idem")
.withPassword("idem")

@DynamicPropertySource
@JvmStatic
fun props(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl)
registry.add("spring.datasource.username", postgres::getUsername)
registry.add("spring.datasource.password", postgres::getPassword)
}
}

class ComplianceQueueRepositoryAdapterTest : SharedPostgresTestBase() {
@Autowired
lateinit var adapter: ComplianceQueueRepositoryAdapter

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
package finance.idem.infrastructure.compliance

import finance.idem.core.TenantId
import finance.idem.infrastructure.SharedPostgresTestBase
import finance.idem.infrastructure.persistence.PersistenceTestConfig
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.math.BigDecimal
import java.time.Instant
import java.time.ZoneOffset
Expand All @@ -23,26 +19,8 @@ import kotlin.test.assertNull

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@Import(LgpdRetentionRepositoryAdapter::class, LgpdRetentionService::class, PersistenceTestConfig::class)
class LgpdRetentionServiceTest {
companion object {
@Container
val postgres =
PostgreSQLContainer("postgres:16")
.withDatabaseName("idem_test")
.withUsername("idem")
.withPassword("idem")

@DynamicPropertySource
@JvmStatic
fun props(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl)
registry.add("spring.datasource.username", postgres::getUsername)
registry.add("spring.datasource.password", postgres::getPassword)
}
}

class LgpdRetentionServiceTest : SharedPostgresTestBase() {
@Autowired lateinit var adapter: LgpdRetentionRepositoryAdapter

@Autowired lateinit var service: LgpdRetentionService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,23 @@ import finance.idem.core.compliance.LegalPerson
import finance.idem.core.compliance.NaturalPerson
import finance.idem.core.compliance.TravelRuleData
import finance.idem.core.compliance.VaspTransferParty
import finance.idem.infrastructure.SharedPostgresTestBase
import finance.idem.infrastructure.persistence.PersistenceTestConfig
import jakarta.persistence.EntityManager
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.time.LocalDate
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@Import(TravelRuleRepositoryAdapter::class, PersistenceTestConfig::class)
class TravelRuleRepositoryAdapterTest {
companion object {
@Container
val postgres =
PostgreSQLContainer("postgres:16")
.withDatabaseName("idem_test")
.withUsername("idem")
.withPassword("idem")

@DynamicPropertySource
@JvmStatic
fun props(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl)
registry.add("spring.datasource.username", postgres::getUsername)
registry.add("spring.datasource.password", postgres::getPassword)
}
}

class TravelRuleRepositoryAdapterTest : SharedPostgresTestBase() {
@Autowired
lateinit var adapter: TravelRuleRepositoryAdapter

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,13 @@ import finance.idem.core.FiatCurrency
import finance.idem.core.TenantId
import finance.idem.core.ledger.Account
import finance.idem.core.ledger.AccountType
import finance.idem.infrastructure.SharedPostgresTestBase
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.context.annotation.Import
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertFalse
Expand All @@ -25,26 +21,8 @@ import kotlin.test.assertTrue

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@Import(AccountRepositoryAdapter::class)
class AccountRepositoryAdapterTest {
companion object {
@Container
val postgres =
PostgreSQLContainer("postgres:16")
.withDatabaseName("idem_test")
.withUsername("idem")
.withPassword("idem")

@DynamicPropertySource
@JvmStatic
fun props(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl)
registry.add("spring.datasource.username", postgres::getUsername)
registry.add("spring.datasource.password", postgres::getPassword)
}
}

class AccountRepositoryAdapterTest : SharedPostgresTestBase() {
@Autowired lateinit var adapter: AccountRepositoryAdapter

private val tenantA = TenantId.generate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@ import finance.idem.core.ledger.AccountType
import finance.idem.core.ledger.JournalLine
import finance.idem.core.ledger.Transaction
import finance.idem.core.monetary.FiatEntry
import finance.idem.infrastructure.SharedPostgresTestBase
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.time.Instant
import java.util.UUID
import kotlin.test.assertEquals
Expand All @@ -32,31 +28,13 @@ import kotlin.test.assertTrue

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@Import(
JournalLineRepositoryAdapter::class,
TransactionRepositoryAdapter::class,
AccountRepositoryAdapter::class,
PersistenceTestConfig::class,
)
class JournalLineRepositoryAdapterTest {
companion object {
@Container
val postgres =
PostgreSQLContainer("postgres:16")
.withDatabaseName("idem_test")
.withUsername("idem")
.withPassword("idem")

@DynamicPropertySource
@JvmStatic
fun props(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl)
registry.add("spring.datasource.username", postgres::getUsername)
registry.add("spring.datasource.password", postgres::getPassword)
}
}

class JournalLineRepositoryAdapterTest : SharedPostgresTestBase() {
@Autowired lateinit var journalLineAdapter: JournalLineRepositoryAdapter

@Autowired lateinit var transactionAdapter: TransactionRepositoryAdapter
Expand Down
Loading