#![allow(clippy::unwrap_used)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::unimplemented)] #![allow(missing_docs)] #![allow(clippy::panic)]
use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait;
use fraiseql_core::{
db::{
DatabaseAdapter, DatabaseType, WhereClause,
traits::{CursorValue, RelayDatabaseAdapter, RelayPageResult},
types::{JsonbValue, OrderByClause, PoolMetrics},
},
error::Result as FraiseQLResult,
schema::{CompiledSchema, SecurityConfig, SqlProjectionHint, TenancyConfig, TenancyMode},
};
use fraiseql_server::{Server, server_config::ServerConfig};
#[derive(Debug, Clone)]
struct NoopRelayAdapter;
#[async_trait]
impl DatabaseAdapter for NoopRelayAdapter {
async fn execute_where_query(
&self,
_view: &str,
_where_clause: Option<&WhereClause>,
_limit: Option<u32>,
_offset: Option<u32>,
_order_by: Option<&[OrderByClause]>,
) -> FraiseQLResult<Vec<JsonbValue>> {
Ok(vec![])
}
async fn execute_with_projection(
&self,
_view: &str,
_projection: Option<&SqlProjectionHint>,
_where_clause: Option<&WhereClause>,
_limit: Option<u32>,
_offset: Option<u32>,
_order_by: Option<&[OrderByClause]>,
) -> FraiseQLResult<Vec<JsonbValue>> {
Ok(vec![])
}
fn database_type(&self) -> DatabaseType {
DatabaseType::PostgreSQL
}
async fn health_check(&self) -> FraiseQLResult<()> {
Ok(())
}
fn pool_metrics(&self) -> PoolMetrics {
PoolMetrics::default()
}
async fn execute_raw_query(
&self,
_sql: &str,
) -> FraiseQLResult<Vec<HashMap<String, serde_json::Value>>> {
Ok(vec![])
}
async fn execute_parameterized_aggregate(
&self,
_sql: &str,
_params: &[serde_json::Value],
) -> FraiseQLResult<Vec<HashMap<String, serde_json::Value>>> {
Ok(vec![])
}
}
#[async_trait::async_trait]
impl fraiseql_core::db::traits::Writer for NoopRelayAdapter {
async fn execute_write(
&self,
request: &fraiseql_core::db::traits::WriteRequest<'_>,
gate: fraiseql_core::db::traits::MutationRowGate<'_>,
) -> std::result::Result<
Vec<std::collections::HashMap<String, serde_json::Value>>,
fraiseql_core::error::FraiseQLError,
> {
let _ = (request, gate);
Err(fraiseql_core::error::FraiseQLError::Unsupported {
message: "this test double does not write".to_string(),
})
}
}
impl RelayDatabaseAdapter for NoopRelayAdapter {
#[allow(clippy::too_many_arguments)] async fn execute_relay_page(
&self,
_view: &str,
_cursor_column: &str,
_after: Option<CursorValue>,
_before: Option<CursorValue>,
_limit: u32,
_forward: bool,
_where_clause: Option<&WhereClause>,
_order_by: Option<&[OrderByClause]>,
_include_total_count: bool,
) -> FraiseQLResult<RelayPageResult> {
unimplemented!("relay queries are never executed in constructor-drift tests")
}
}
fn incompatible_schema() -> CompiledSchema {
let mut schema = CompiledSchema::new();
schema.fraiseql_version = serde_json::from_value(serde_json::json!("2.14.0")).unwrap();
schema
}
fn current_schema() -> CompiledSchema {
CompiledSchema::new()
}
fn multi_tenant_schema_without_rls() -> CompiledSchema {
let mut schema = current_schema();
schema.security = Some(SecurityConfig {
tenancy: TenancyConfig {
mode: TenancyMode::Row,
..TenancyConfig::default()
},
..SecurityConfig::default()
});
schema
}
fn caching_config() -> ServerConfig {
ServerConfig {
cache_enabled: true,
cors_enabled: false,
..ServerConfig::default()
}
}
#[tokio::test]
async fn server_new_refuses_incompatible_schema_format() {
let config = ServerConfig {
cache_enabled: false,
cors_enabled: false,
..ServerConfig::default()
};
let result = Server::new(config, incompatible_schema(), Arc::new(NoopRelayAdapter), None).await;
assert!(
result.is_err(),
"Server::new must refuse an incompatible compiled-schema version"
);
}
#[tokio::test]
async fn with_relay_pagination_refuses_incompatible_schema_format() {
let config = ServerConfig {
cache_enabled: false,
cors_enabled: false,
..ServerConfig::default()
};
let result = Server::with_relay_pagination(
config,
incompatible_schema(),
Arc::new(NoopRelayAdapter),
None,
)
.await;
assert!(
result.is_err(),
"with_relay_pagination must refuse an incompatible compiled-schema version (H16)"
);
}
#[tokio::test]
async fn with_relay_pagination_accepts_current_schema_format() {
let config = ServerConfig {
cache_enabled: false,
cors_enabled: false,
..ServerConfig::default()
};
let result =
Server::with_relay_pagination(config, current_schema(), Arc::new(NoopRelayAdapter), None)
.await;
assert!(result.is_ok(), "with_relay_pagination must accept a current-version schema");
}
#[cfg(feature = "arrow")]
#[tokio::test]
async fn with_flight_service_refuses_incompatible_schema_format() {
let config = ServerConfig {
cache_enabled: false,
cors_enabled: false,
..ServerConfig::default()
};
let result = Server::with_flight_service(
config,
incompatible_schema(),
Arc::new(NoopRelayAdapter),
None,
None,
)
.await;
assert!(
result.is_err(),
"with_flight_service must refuse an incompatible compiled-schema version (H16)"
);
}
#[tokio::test]
async fn server_new_refuses_multi_tenant_caching_without_rls() {
let result = Server::new(
caching_config(),
multi_tenant_schema_without_rls(),
Arc::new(NoopRelayAdapter),
None,
)
.await;
let Err(e) = result else {
panic!("#758: Server::new must refuse multi-tenant + cache + no RLS");
};
let msg = e.to_string();
assert!(msg.contains("multi-tenant"), "the refusal must say why: {msg}");
assert!(
msg.contains("[security.rls]"),
"the refusal must name a knob that exists: {msg}"
);
}
#[tokio::test]
async fn with_relay_pagination_refuses_multi_tenant_caching_without_rls() {
let result = Server::with_relay_pagination(
caching_config(),
multi_tenant_schema_without_rls(),
Arc::new(NoopRelayAdapter),
None,
)
.await;
assert!(
result.is_err(),
"#758: with_relay_pagination must refuse multi-tenant + cache + no RLS"
);
}
#[tokio::test]
async fn a_single_tenant_schema_still_boots_with_caching() {
let result =
Server::new(caching_config(), current_schema(), Arc::new(NoopRelayAdapter), None).await;
assert!(result.is_ok(), "single-tenant + cache must still boot: {:?}", result.err());
}
#[tokio::test]
async fn declaring_rls_changes_the_outcome() {
let mut schema = multi_tenant_schema_without_rls();
if let Some(security) = schema.security.as_mut() {
security.rls.enabled = true;
}
let refused_without = Server::new(
caching_config(),
multi_tenant_schema_without_rls(),
Arc::new(NoopRelayAdapter),
None,
)
.await
.is_err();
assert!(refused_without, "precondition: the undeclared schema is refused");
let result = Server::new(caching_config(), schema, Arc::new(NoopRelayAdapter), None).await;
assert!(
result.is_ok(),
"declaring [security.rls] must change the outcome: {:?}",
result.err()
);
}
fn unattributable_mutation_schema() -> CompiledSchema {
use fraiseql_core::schema::{MutationDefinition, QueryDefinition, TypeDefinition};
let mut schema = current_schema();
schema.types.push(TypeDefinition::new("Price", "v_price"));
schema.types.push(TypeDefinition::new("RebuildResult", ""));
let mut query = QueryDefinition::new("prices", "Price");
query.sql_source = Some("v_price".to_string());
query.cache_ttl_seconds = Some(0);
schema.queries.push(query);
schema
.mutations
.push(MutationDefinition::new("rebuildPricing", "RebuildResult"));
schema
}
#[tokio::test]
async fn an_unattributable_mutation_refuses_to_boot_with_the_cache_on() {
let err = Server::new(
caching_config(),
unattributable_mutation_schema(),
Arc::new(NoopRelayAdapter),
None,
)
.await
.err()
.expect("a mutation that resolves to no view must refuse to boot with caching on (#910)");
let msg = err.to_string();
assert!(
msg.contains("rebuildPricing") && msg.contains("invalidates_views"),
"the refusal must name the mutation and the fix; got: {msg}"
);
}
#[tokio::test]
async fn the_same_schema_boots_with_the_cache_off() {
let config = ServerConfig {
cache_enabled: false,
cors_enabled: false,
..ServerConfig::default()
};
let result =
Server::new(config, unattributable_mutation_schema(), Arc::new(NoopRelayAdapter), None)
.await;
assert!(result.is_ok(), "cache_enabled = false must still boot: {:?}", result.err());
}
#[tokio::test]
async fn declaring_invalidates_views_lets_it_boot() {
let mut schema = unattributable_mutation_schema();
schema.mutations[0].invalidates_views = vec!["v_price".to_string()];
let result = Server::new(caching_config(), schema, Arc::new(NoopRelayAdapter), None).await;
assert!(
result.is_ok(),
"declaring what the mutation writes must change the outcome: {:?}",
result.err()
);
}