mod common;
use std::sync::Arc;
use fraiseql_core::{db::postgres::PostgresAdapter, schema::CompiledSchema};
use fraiseql_test_utils::failing_adapter::FailingAdapter;
use reqwest::Client;
use serde_json::{Value, json};
use testcontainers_modules::{postgres::Postgres, testcontainers::runners::AsyncRunner};
use crate::common::server_harness::TestServer;
fn user_schema_with_federation() -> CompiledSchema {
let json = r#"{
"types": [
{
"name": "User",
"table": "\"user\"",
"sql_source": "v_user",
"fields": [
{"name": "id", "field_type": "ID", "nullable": false},
{"name": "name", "field_type": "String", "nullable": false}
]
}
],
"queries": [
{
"name": "user",
"return_type": "User",
"returns_list": false,
"nullable": true,
"arguments": [
{"name": "id", "arg_type": "ID", "nullable": false}
]
}
],
"mutations": [],
"subscriptions": [],
"federation": {
"enabled": true,
"version": "v2",
"service_name": "users",
"entities": [
{"name": "User", "key_fields": ["id"]}
]
},
"schema_sdl": "type User {\n id: ID!\n name: String!\n}\n\ntype Query {\n user(id: ID!): User\n}\n"
}"#;
CompiledSchema::from_json(json).expect("test schema must be valid")
}
async fn setup_users_table(port: u16) -> tokio_postgres::Client {
let (client, conn) = tokio_postgres::connect(
&format!("host=127.0.0.1 port={port} user=testuser password=testpw dbname=testdb"),
tokio_postgres::NoTls,
)
.await
.expect("connect to test postgres");
tokio::spawn(async move { conn.await.ok() });
client
.batch_execute(
r#"CREATE TABLE IF NOT EXISTS "user" (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
__typename TEXT DEFAULT 'User'
);"#,
)
.await
.expect("create user table");
client
}
#[tokio::test]
async fn service_sdl_contains_federation_directives() {
let schema = user_schema_with_federation();
let adapter = Arc::new(FailingAdapter::new());
let server = TestServer::start(schema, adapter).await;
let client = Client::new();
let resp = client
.post(format!("{}/graphql", server.url))
.json(&json!({"query": "{ _service { sdl } }"}))
.send()
.await
.expect("request failed")
.json::<Value>()
.await
.expect("json parse failed");
assert!(resp["errors"].is_null(), "unexpected errors: {}", resp["errors"]);
let sdl = resp["data"]["_service"]["sdl"].as_str().expect("_service.sdl must be a string");
assert!(
sdl.contains("@key(fields: \"id\")"),
"SDL must contain inline @key directive, got:\n{sdl}"
);
assert!(sdl.contains("type User"), "SDL must define User type");
assert!(sdl.contains("_entities"), "SDL must declare _entities query");
assert!(sdl.contains("_service"), "SDL must declare _service query");
assert!(!sdl.contains("# @key"), "SDL must not contain commented @key: {sdl}");
}
#[tokio::test]
async fn entities_resolves_user_by_id() {
if std::env::var("FEDERATION_TESTS").is_err() {
eprintln!("Skipping: FEDERATION_TESTS not set");
return;
}
let pg = Postgres::default()
.with_user("testuser")
.with_password("testpw")
.with_db_name("testdb")
.start()
.await
.expect("start postgres container");
let pg_port = pg.get_host_port_ipv4(5432).await.expect("postgres port");
let pg_client = setup_users_table(pg_port).await;
pg_client
.execute(
r#"INSERT INTO "user" (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING"#,
&[&"user-alice", &"Alice"],
)
.await
.expect("insert test user");
let db_url = format!("postgresql://testuser:testpw@127.0.0.1:{pg_port}/testdb");
let adapter = Arc::new(PostgresAdapter::new(&db_url).await.expect("postgres adapter"));
let schema = user_schema_with_federation();
let server = TestServer::start(schema, adapter).await;
let http = Client::new();
let resp = http
.post(format!("{}/graphql", server.url))
.json(&json!({
"query": "query($repr: [_Any!]!) { _entities(representations: $repr) { ... on User { id name } } }",
"variables": {
"repr": [{"__typename": "User", "id": "user-alice"}]
}
}))
.send()
.await
.expect("request failed")
.json::<Value>()
.await
.expect("json parse failed");
assert!(resp["errors"].is_null(), "unexpected errors: {}", resp["errors"]);
assert_eq!(resp["data"]["_entities"][0]["name"], "Alice", "full response: {resp}");
}
#[tokio::test]
async fn apollo_router_routes_query_to_fraiseql_subgraph() {
use testcontainers::{GenericImage, ImageExt as _, core::WaitFor};
if std::env::var("FEDERATION_TESTS").is_err() {
eprintln!("Skipping: FEDERATION_TESTS not set");
return;
}
let pg = Postgres::default()
.with_user("testuser")
.with_password("testpw")
.with_db_name("testdb")
.start()
.await
.expect("start postgres container");
let pg_port = pg.get_host_port_ipv4(5432).await.expect("postgres port");
let pg_client = setup_users_table(pg_port).await;
pg_client
.execute(
r#"INSERT INTO "user" (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING"#,
&[&"user-bob", &"Bob"],
)
.await
.expect("insert test user");
let db_url = format!("postgresql://testuser:testpw@127.0.0.1:{pg_port}/testdb");
let adapter = Arc::new(PostgresAdapter::new(&db_url).await.expect("postgres adapter"));
let schema = user_schema_with_federation();
let server = TestServer::start(schema, adapter).await;
let supergraph =
include_str!("../../fraiseql-core/tests/federation/fixtures/supergraph_single.graphql")
.replace("__SUBGRAPH_URL__", &format!("{}/graphql", server.url));
let supergraph_file = tempfile::NamedTempFile::new().expect("tmpfile");
std::fs::write(supergraph_file.path(), &supergraph).expect("write supergraph");
let _router = GenericImage::new("ghcr.io/apollographql/router", "v1.45.0")
.with_wait_for(WaitFor::message_on_stderr("GraphQL endpoint exposed"))
.with_network("host")
.with_env_var("APOLLO_ROUTER_SUPERGRAPH_PATH", "/supergraph.graphql")
.start()
.await
.expect("start Apollo Router container");
let _ = supergraph_file;
let http = Client::new();
let resp = http
.post("http://127.0.0.1:4000/graphql")
.json(&json!({"query": "{ user(id: \"user-bob\") { id name } }"}))
.send()
.await
.expect("gateway request failed")
.json::<Value>()
.await
.expect("json parse failed");
assert!(resp["errors"].is_null(), "gateway errors: {}", resp["errors"]);
assert_eq!(resp["data"]["user"]["name"], "Bob");
}
#[tokio::test]
async fn entities_returns_null_for_missing_entity() {
let schema = user_schema_with_federation();
let adapter = Arc::new(FailingAdapter::new());
let server = TestServer::start(schema, adapter).await;
let http = Client::new();
let resp = http
.post(format!("{}/graphql", server.url))
.json(&json!({
"query": "query($representations: [_Any!]!) { _entities(representations: $representations) { ... on User { id } } }",
"variables": {
"representations": [{"__typename": "User", "id": "00000000-0000-0000-0000-000000000000"}]
}
}))
.send()
.await
.expect("request failed")
.json::<Value>()
.await
.expect("json parse failed");
assert!(resp["errors"].is_null(), "unexpected errors: {}", resp["errors"]);
assert!(
resp["data"]["_entities"][0].is_null(),
"missing entity should be null, got: {}",
resp["data"]["_entities"][0]
);
}