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 {
|
||||
private val tl = ThreadLocal<Tenant?>()
|
||||
|
||||
fun set(tenant: Tenant) { tl.set(tenant) }
|
||||
fun set(tenant: Tenant?) { tl.set(tenant) }
|
||||
fun clear() { tl.remove() }
|
||||
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
|
||||
|
||||
import at.mocode.entries.domain.model.Nennung
|
||||
import at.mocode.entries.domain.repository.NennungRepository
|
||||
import at.mocode.entries.service.persistence.NennungTable
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -33,7 +31,6 @@ import org.testcontainers.containers.PostgreSQLContainer
|
|||
import org.testcontainers.junit.jupiter.Container
|
||||
import org.testcontainers.junit.jupiter.Testcontainers
|
||||
import kotlin.time.Clock
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
|
|
@ -65,8 +62,7 @@ import kotlin.uuid.Uuid
|
|||
@Testcontainers
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||
private val jdbcTemplate: JdbcTemplate,
|
||||
private val nennungRepository: NennungRepository
|
||||
private val jdbcTemplate: JdbcTemplate
|
||||
) {
|
||||
|
||||
@TestConfiguration
|
||||
|
|
@ -76,6 +72,10 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val SCHEMA_A = "event_a"
|
||||
private const val SCHEMA_B = "event_b"
|
||||
private const val CONTROL_SCHEMA = "control"
|
||||
|
||||
@Container
|
||||
@JvmStatic
|
||||
val postgres = PostgreSQLContainer<Nothing>("postgres:16-alpine").apply {
|
||||
|
|
@ -86,6 +86,7 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
|||
|
||||
@JvmStatic
|
||||
@DynamicPropertySource
|
||||
@Suppress("unused")
|
||||
fun registerDataSource(registry: DynamicPropertyRegistry) {
|
||||
// Ensure the container is started before accessing dynamic properties
|
||||
if (!postgres.isRunning) {
|
||||
|
|
@ -110,15 +111,17 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
|||
.migrate()
|
||||
|
||||
// Zwei Tenants registrieren
|
||||
jdbcTemplate.update("CREATE SCHEMA IF NOT EXISTS \"event_a\"")
|
||||
jdbcTemplate.update("CREATE SCHEMA IF NOT EXISTS \"event_b\"")
|
||||
jdbcTemplate.update("CREATE SCHEMA IF NOT EXISTS \"$SCHEMA_A\"")
|
||||
jdbcTemplate.update("CREATE SCHEMA IF NOT EXISTS \"$SCHEMA_B\"")
|
||||
|
||||
// Use explicit schema mapping and column names to avoid resolution issues in tests
|
||||
jdbcTemplate.update("INSERT INTO \"control\".\"tenants\" (event_id, schema_name, db_url, status) VALUES ('event_a', 'event_a', null, 'ACTIVE')")
|
||||
jdbcTemplate.update("INSERT INTO \"control\".\"tenants\" (event_id, schema_name, db_url, status) VALUES ('event_b', 'event_b', null, 'ACTIVE')")
|
||||
// Use explicit schema mapping and column names to avoid resolution of issues in tests
|
||||
@Suppress("SqlResolve")
|
||||
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)
|
||||
listOf("event_a", "event_b").forEach { schema ->
|
||||
listOf(SCHEMA_A, SCHEMA_B).forEach { schema ->
|
||||
jdbcTemplate.update("""
|
||||
CREATE TABLE IF NOT EXISTS "$schema"."nennungen" (
|
||||
"id" UUID PRIMARY KEY,
|
||||
|
|
@ -143,13 +146,14 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
|||
@Test
|
||||
fun `writes in tenant A are not visible in tenant B`() = runBlocking {
|
||||
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
|
||||
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 {
|
||||
// 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")
|
||||
|
||||
NennungTable.insert {
|
||||
|
|
@ -167,46 +171,20 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
|||
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
|
||||
TenantContextHolder.set(Tenant(eventId = "event_b", schemaName = "event_b"))
|
||||
try {
|
||||
TenantContextHolder.withTenant(tenantB) {
|
||||
val countB = tenantTransaction {
|
||||
TransactionManager.current().exec("SET search_path TO \"event_b\", pg_catalog")
|
||||
NennungTable.selectAll().count()
|
||||
}
|
||||
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.
|
||||
- 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.
|
||||
- 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
|
||||
- `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