From 765680969cf9ffe3e179c6fdf95a57039c765781 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 10:25:19 +0900 Subject: [PATCH 1/3] test(state): forbid public raw-SQL repository escape hatch --- tests/postgres_repository_api_boundary.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/postgres_repository_api_boundary.rs diff --git a/tests/postgres_repository_api_boundary.rs b/tests/postgres_repository_api_boundary.rs new file mode 100644 index 00000000..54f4dbca --- /dev/null +++ b/tests/postgres_repository_api_boundary.rs @@ -0,0 +1,18 @@ +#[test] +fn production_postgres_repository_does_not_expose_generic_raw_sql_transaction_surface() { + let source = std::fs::read_to_string("src/postgres_state.rs") + .expect("PostgreSQL repository source must exist"); + + for forbidden in [ + "pub struct TenantTransaction", + "pub type TenantTransactionFuture", + "pub async fn query_scalar_i64", + "pub async fn query_scalar_text", + "pub async fn with_tenant_transaction", + ] { + assert!( + !source.contains(forbidden), + "production PostgreSQL repository API must not expose generic raw-SQL transaction primitive `{forbidden}`; tenant-scoped state access must remain behind typed repository methods" + ); + } +} From 9cd919b2f87f6049275878610353f1d56af02be3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 10:39:32 +0900 Subject: [PATCH 2/3] fix(state): make raw SQL transaction seam crate-private --- src/postgres_state.rs | 36 ++++++++++++------- .../postgres_tenant_context_pool.rs | 0 2 files changed, 23 insertions(+), 13 deletions(-) rename {tests => test_support}/postgres_tenant_context_pool.rs (100%) diff --git a/src/postgres_state.rs b/src/postgres_state.rs index 0c184d23..5cde5f95 100644 --- a/src/postgres_state.rs +++ b/src/postgres_state.rs @@ -238,25 +238,25 @@ pub struct PostgresTenantPool { inner: Arc, } -/// Borrowed transaction surface. Callers can execute only repository-owned fixed SQL here; -/// tenant identity binding itself is always parameterized by [`PostgresTenantPool`]. -pub struct TenantTransaction<'client> { +/// Crate-private borrowed transaction seam for typed repository methods and white-box tests. +/// Ordinary application callers cannot submit SQL through this surface. +pub(crate) struct TenantTransaction<'client> { client: &'client Client, } -/// Boxed operation future used to keep the tenant transaction borrow scoped to one checkout. -pub type TenantTransactionFuture<'client, T> = +/// Crate-private boxed future keeping a tenant transaction borrow scoped to one checkout. +pub(crate) type TenantTransactionFuture<'client, T> = Pin> + Send + 'client>>; impl<'client> TenantTransaction<'client> { - /// Execute fixed repository SQL that returns one `BIGINT` scalar. - pub async fn query_scalar_i64(&self, sql: &str) -> PostgresStateResult { + /// Execute fixed white-box test SQL returning one `BIGINT` scalar. + pub(crate) async fn query_scalar_i64(&self, sql: &str) -> PostgresStateResult { let row = self.client.query_one(sql, &[]).await?; Ok(row.try_get(0)?) } - /// Execute fixed repository SQL that returns one non-null text scalar. - pub async fn query_scalar_text(&self, sql: &str) -> PostgresStateResult { + /// Execute fixed white-box test SQL returning one non-null text scalar. + pub(crate) async fn query_scalar_text(&self, sql: &str) -> PostgresStateResult { let row = self.client.query_one(sql, &[]).await?; Ok(row.try_get(0)?) } @@ -394,10 +394,10 @@ impl PostgresTenantPool { /// Run one operation inside a transaction whose tenant identity is local to that transaction. /// - /// The connection stays locked from `BEGIN` through `COMMIT`/`ROLLBACK`. Cancellation drops - /// the scope, which transfers the owned checkout into a rollback task; the mutex therefore - /// cannot be reacquired until rollback has completed. - pub async fn with_tenant_transaction( + /// This seam is crate-private: production callers use typed repository methods. It remains + /// available to the colocated real-PostgreSQL white-box suite so commit/error/cancellation + /// cleanup can be verified without exporting an arbitrary-SQL application API. + pub(crate) async fn with_tenant_transaction( &self, tenant_id: &TenantId, operation: F, @@ -601,6 +601,16 @@ impl Drop for ActiveTransaction { } } +#[cfg(test)] +mod tenant_context_integration_tests { + use crate as waf_ids_ai_soc; + + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_support/postgres_tenant_context_pool.rs" + )); +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/postgres_tenant_context_pool.rs b/test_support/postgres_tenant_context_pool.rs similarity index 100% rename from tests/postgres_tenant_context_pool.rs rename to test_support/postgres_tenant_context_pool.rs From da5f2b01163e9db1416bfa4162ebdb7953778fc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 10:46:58 +0900 Subject: [PATCH 3/3] fix(state): compile white-box queries only in tests --- src/postgres_state.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/postgres_state.rs b/src/postgres_state.rs index 5cde5f95..bb89a50f 100644 --- a/src/postgres_state.rs +++ b/src/postgres_state.rs @@ -250,12 +250,14 @@ pub(crate) type TenantTransactionFuture<'client, T> = impl<'client> TenantTransaction<'client> { /// Execute fixed white-box test SQL returning one `BIGINT` scalar. + #[cfg(test)] pub(crate) async fn query_scalar_i64(&self, sql: &str) -> PostgresStateResult { let row = self.client.query_one(sql, &[]).await?; Ok(row.try_get(0)?) } /// Execute fixed white-box test SQL returning one non-null text scalar. + #[cfg(test)] pub(crate) async fn query_scalar_text(&self, sql: &str) -> PostgresStateResult { let row = self.client.query_one(sql, &[]).await?; Ok(row.try_get(0)?)