chore(tests+config): enhance EntriesIsolationIntegrationTest and add missing Spring metadata
- Improved schema isolation logic with constants for tenant schemas and search path management in PostgreSQL. - Added `withTenant` utility in `TenantContextHolder` to simplify tenant context usage. - Removed unused imports, variables, and helper functions (`random()` and redundant `NennungRepository` references). - Included missing `multitenancy.*` configuration keys in `additional-spring-configuration-metadata.json` to address IDE warnings.
This commit is contained in:
parent
f961b6e771
commit
a15cc5971f
|
|
@ -3,7 +3,17 @@ package at.mocode.entries.service.tenant
|
||||||
object TenantContextHolder {
|
object TenantContextHolder {
|
||||||
private val tl = ThreadLocal<Tenant?>()
|
private val tl = ThreadLocal<Tenant?>()
|
||||||
|
|
||||||
fun set(tenant: Tenant) { tl.set(tenant) }
|
fun set(tenant: Tenant?) { tl.set(tenant) }
|
||||||
fun clear() { tl.remove() }
|
fun clear() { tl.remove() }
|
||||||
fun current(): Tenant? = tl.get()
|
fun current(): Tenant? = tl.get()
|
||||||
|
|
||||||
|
inline fun <T> withTenant(tenant: Tenant, block: () -> T): T {
|
||||||
|
val old = current()
|
||||||
|
set(tenant)
|
||||||
|
try {
|
||||||
|
return block()
|
||||||
|
} finally {
|
||||||
|
set(old)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "multitenancy.registry.type",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "Type of tenant registry (jdbc or inmem).",
|
||||||
|
"defaultValue": "jdbc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multitenancy.defaultSchemas",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "Comma-separated list of default schemas for inmem registry.",
|
||||||
|
"defaultValue": "public"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
package at.mocode.entries.service.tenant
|
package at.mocode.entries.service.tenant
|
||||||
|
|
||||||
import at.mocode.entries.domain.model.Nennung
|
|
||||||
import at.mocode.entries.domain.repository.NennungRepository
|
|
||||||
import at.mocode.entries.service.persistence.NennungTable
|
import at.mocode.entries.service.persistence.NennungTable
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
@ -33,7 +31,6 @@ import org.testcontainers.containers.PostgreSQLContainer
|
||||||
import org.testcontainers.junit.jupiter.Container
|
import org.testcontainers.junit.jupiter.Container
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers
|
import org.testcontainers.junit.jupiter.Testcontainers
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
@ExtendWith(SpringExtension::class)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||||
|
|
@ -65,8 +62,7 @@ import kotlin.uuid.Uuid
|
||||||
@Testcontainers
|
@Testcontainers
|
||||||
@TestInstance(Lifecycle.PER_CLASS)
|
@TestInstance(Lifecycle.PER_CLASS)
|
||||||
class EntriesIsolationIntegrationTest @Autowired constructor(
|
class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||||
private val jdbcTemplate: JdbcTemplate,
|
private val jdbcTemplate: JdbcTemplate
|
||||||
private val nennungRepository: NennungRepository
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
|
|
@ -76,6 +72,10 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val SCHEMA_A = "event_a"
|
||||||
|
private const val SCHEMA_B = "event_b"
|
||||||
|
private const val CONTROL_SCHEMA = "control"
|
||||||
|
|
||||||
@Container
|
@Container
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val postgres = PostgreSQLContainer<Nothing>("postgres:16-alpine").apply {
|
val postgres = PostgreSQLContainer<Nothing>("postgres:16-alpine").apply {
|
||||||
|
|
@ -86,6 +86,7 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@DynamicPropertySource
|
@DynamicPropertySource
|
||||||
|
@Suppress("unused")
|
||||||
fun registerDataSource(registry: DynamicPropertyRegistry) {
|
fun registerDataSource(registry: DynamicPropertyRegistry) {
|
||||||
// Ensure the container is started before accessing dynamic properties
|
// Ensure the container is started before accessing dynamic properties
|
||||||
if (!postgres.isRunning) {
|
if (!postgres.isRunning) {
|
||||||
|
|
@ -110,15 +111,17 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||||
.migrate()
|
.migrate()
|
||||||
|
|
||||||
// Zwei Tenants registrieren
|
// Zwei Tenants registrieren
|
||||||
jdbcTemplate.update("CREATE SCHEMA IF NOT EXISTS \"event_a\"")
|
jdbcTemplate.update("CREATE SCHEMA IF NOT EXISTS \"$SCHEMA_A\"")
|
||||||
jdbcTemplate.update("CREATE SCHEMA IF NOT EXISTS \"event_b\"")
|
jdbcTemplate.update("CREATE SCHEMA IF NOT EXISTS \"$SCHEMA_B\"")
|
||||||
|
|
||||||
// Use explicit schema mapping and column names to avoid resolution issues in tests
|
// Use explicit schema mapping and column names to avoid resolution of issues in tests
|
||||||
jdbcTemplate.update("INSERT INTO \"control\".\"tenants\" (event_id, schema_name, db_url, status) VALUES ('event_a', 'event_a', null, 'ACTIVE')")
|
@Suppress("SqlResolve")
|
||||||
jdbcTemplate.update("INSERT INTO \"control\".\"tenants\" (event_id, schema_name, db_url, status) VALUES ('event_b', 'event_b', null, 'ACTIVE')")
|
jdbcTemplate.update("INSERT INTO \"$CONTROL_SCHEMA\".\"tenants\" (event_id, schema_name, db_url, status) VALUES ('$SCHEMA_A', '$SCHEMA_A', null, 'ACTIVE')")
|
||||||
|
@Suppress("SqlResolve")
|
||||||
|
jdbcTemplate.update("INSERT INTO \"$CONTROL_SCHEMA\".\"tenants\" (event_id, schema_name, db_url, status) VALUES ('$SCHEMA_B', '$SCHEMA_B', null, 'ACTIVE')")
|
||||||
|
|
||||||
// Tenant-Tabellen in beiden Schemas erstellen (über JDBC statt Exposed, um Meta-Binding zu vermeiden)
|
// Tenant-Tabellen in beiden Schemas erstellen (über JDBC statt Exposed, um Meta-Binding zu vermeiden)
|
||||||
listOf("event_a", "event_b").forEach { schema ->
|
listOf(SCHEMA_A, SCHEMA_B).forEach { schema ->
|
||||||
jdbcTemplate.update("""
|
jdbcTemplate.update("""
|
||||||
CREATE TABLE IF NOT EXISTS "$schema"."nennungen" (
|
CREATE TABLE IF NOT EXISTS "$schema"."nennungen" (
|
||||||
"id" UUID PRIMARY KEY,
|
"id" UUID PRIMARY KEY,
|
||||||
|
|
@ -143,13 +146,14 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||||
@Test
|
@Test
|
||||||
fun `writes in tenant A are not visible in tenant B`() = runBlocking {
|
fun `writes in tenant A are not visible in tenant B`() = runBlocking {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
|
val tenantA = Tenant(eventId = "event_a", schemaName = "event_a")
|
||||||
|
val tenantB = Tenant(eventId = "event_b", schemaName = "event_b")
|
||||||
|
|
||||||
// Tenant A: Save via Exposed raw to avoid repository complexities
|
// Tenant A: Save via Exposed raw to avoid repository complexities
|
||||||
TenantContextHolder.set(Tenant(eventId = "event_a", schemaName = "event_a"))
|
|
||||||
try {
|
|
||||||
val nennungIdA = java.util.UUID.randomUUID()
|
val nennungIdA = java.util.UUID.randomUUID()
|
||||||
|
TenantContextHolder.withTenant(tenantA) {
|
||||||
tenantTransaction {
|
tenantTransaction {
|
||||||
// Double-check search_path
|
// Double-check search_path manually if tenantTransaction might be using a cached connection or different schema binding
|
||||||
TransactionManager.current().exec("SET search_path TO \"event_a\", pg_catalog")
|
TransactionManager.current().exec("SET search_path TO \"event_a\", pg_catalog")
|
||||||
|
|
||||||
NennungTable.insert {
|
NennungTable.insert {
|
||||||
|
|
@ -167,46 +171,20 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||||
it[updatedAt] = now
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifiziere per JDBC, dass es wirklich in event_a gelandet ist
|
|
||||||
val countA = jdbcTemplate.queryForObject<Long>("SELECT count(*) FROM \"event_a\".\"nennungen\"")
|
|
||||||
assertEquals(1L, countA, "Erwartet 1 Nennung in event_a")
|
|
||||||
} finally {
|
|
||||||
TenantContextHolder.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verifiziere per JDBC, dass es wirklich in event_a gelandet ist
|
||||||
|
@Suppress("SqlResolve")
|
||||||
|
val countA = jdbcTemplate.queryForObject<Long>("SELECT count(*) FROM \"$SCHEMA_A\".\"nennungen\"")
|
||||||
|
assertEquals(1L, countA, "Erwartet 1 Nennung in event_a")
|
||||||
|
|
||||||
// Tenant B: Nennungen zählen
|
// Tenant B: Nennungen zählen
|
||||||
TenantContextHolder.set(Tenant(eventId = "event_b", schemaName = "event_b"))
|
TenantContextHolder.withTenant(tenantB) {
|
||||||
try {
|
|
||||||
val countB = tenantTransaction {
|
val countB = tenantTransaction {
|
||||||
TransactionManager.current().exec("SET search_path TO \"event_b\", pg_catalog")
|
TransactionManager.current().exec("SET search_path TO \"event_b\", pg_catalog")
|
||||||
NennungTable.selectAll().count()
|
NennungTable.selectAll().count()
|
||||||
}
|
}
|
||||||
assertEquals(0L, countB, "Erwartet keine Nennungen in Tenant B")
|
assertEquals(0L, countB, "Erwartet keine Nennungen in Tenant B")
|
||||||
} finally {
|
|
||||||
TenantContextHolder.clear()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Quad<A, B, C, D>(val first: A, val second: B, val third: C, val fourth: D)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Kleine Test-Helfer ---
|
|
||||||
private fun Nennung.Companion.random(now: kotlin.time.Instant): Nennung {
|
|
||||||
return Nennung(
|
|
||||||
nennungId = Uuid.random(),
|
|
||||||
abteilungId = Uuid.random(),
|
|
||||||
bewerbId = Uuid.random(),
|
|
||||||
turnierId = Uuid.random(),
|
|
||||||
reiterId = Uuid.random(),
|
|
||||||
pferdId = Uuid.random(),
|
|
||||||
zahlerId = null,
|
|
||||||
status = at.mocode.core.domain.model.NennStatusE.EINGEGANGEN,
|
|
||||||
startwunsch = at.mocode.core.domain.model.StartwunschE.VORNE,
|
|
||||||
istNachnennung = false,
|
|
||||||
nachnenngebuehrErlassen = false,
|
|
||||||
bemerkungen = null,
|
|
||||||
createdAt = now,
|
|
||||||
updatedAt = now
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,10 @@ Zusätzlich gab es IDE-Warnungen bezüglich nicht auflösbarer Symbole in SQL-St
|
||||||
- Explizite Verwendung von `SET search_path` innerhalb der Transaktionen im Isolationstest, um Leaks zu vermeiden.
|
- Explizite Verwendung von `SET search_path` innerhalb der Transaktionen im Isolationstest, um Leaks zu vermeiden.
|
||||||
- Verifizierung der Isolation: Schreibzugriffe in `event_a` landen nun nachweislich nicht mehr in `event_b`.
|
- Verifizierung der Isolation: Schreibzugriffe in `event_a` landen nun nachweislich nicht mehr in `event_b`.
|
||||||
|
|
||||||
3. **Verifizierung:**
|
3. **Verifizierung & Cleanup:**
|
||||||
- Alle 10 Tests im Modul (inkl. der neu aktivierten Isolation-Tests) laufen erfolgreich durch.
|
- Alle 10 Tests im Modul (inkl. der neu aktivierten Isolation-Tests) laufen erfolgreich durch.
|
||||||
- Die IDE-Warnungen wurden durch Verwendung von Double-Quotes (`"control"."tenants"`) und saubere Kotlin-Strukturen minimiert.
|
- IDE-Warnungen wurden durch Verwendung von String-Konstanten/Interpolation (`$CONTROL_SCHEMA`) und Entfernung ungenutzter Code-Fragmente (`nennungRepository`, `random()`) behoben.
|
||||||
|
- Fehlende Spring-Konfigurations-Metadaten für `multitenancy.*` wurden in `additional-spring-configuration-metadata.json` ergänzt.
|
||||||
|
|
||||||
## Betroffene Dateien
|
## Betroffene Dateien
|
||||||
- `backend/services/entries/entries-service/src/test/kotlin/at/mocode/entries/service/tenant/EntriesIsolationIntegrationTest.kt`: Reaktiviert und repariert.
|
- `backend/services/entries/entries-service/src/test/kotlin/at/mocode/entries/service/tenant/EntriesIsolationIntegrationTest.kt`: Reaktiviert und repariert.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user