#![cfg(feature = "graphql")]
use axum::body::Body;
use axum::http::Request;
use axum::Router;
use postrust_core::AppConfig;
use postrust_server::{build, Host};
use serde_json::{json, Value};
use tower::ServiceExt;
const TABLE: &str = "graphql_reload_probe";
const CHANNEL: &str = "graphql_reload_test";
fn database_url() -> String {
std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/postrust_test".into())
}
async fn query(app: &Router, document: &str) -> Value {
let response = app
.clone()
.oneshot(
Request::post("/v1/graphql")
.header("content-type", "application/json")
.body(Body::from(json!({ "query": document }).to_string()))
.unwrap(),
)
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap()
}
async fn reload(admin: &sqlx::PgPool, channel: &str) {
sqlx::query(&format!("NOTIFY {channel}, 'reload schema'"))
.execute(admin)
.await
.unwrap();
}
#[tokio::test]
#[ignore = "requires PostgreSQL"]
async fn a_table_added_after_start_up_is_served_once_notified() {
let admin = sqlx::PgPool::connect(&database_url()).await.unwrap();
sqlx::query(&format!("DROP TABLE IF EXISTS public.{TABLE}"))
.execute(&admin)
.await
.unwrap();
let mut config = AppConfig::default();
config.db_uri = database_url();
config.db_schemas = vec!["public".to_string()];
config.db_anon_role = Some("web_anon".to_string());
config.db_channel_enabled = true;
config.db_channel = CHANNEL.to_string();
let app = build(config, Host::SERVER).await.expect("build").router;
let document = format!("{{ {TABLE} {{ id }} }}");
let before = query(&app, &document).await;
assert!(before["data"].is_null(), "{before}");
for statement in [
format!("CREATE TABLE public.{TABLE} (id int PRIMARY KEY)"),
format!("INSERT INTO public.{TABLE} VALUES (1)"),
format!("GRANT SELECT ON public.{TABLE} TO web_anon"),
] {
sqlx::query(&statement).execute(&admin).await.unwrap();
}
let mut after = Value::Null;
for _ in 0..50 {
reload(&admin, CHANNEL).await;
after = query(&app, &document).await;
if after["data"][TABLE] == json!([{ "id": 1 }]) {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
assert_eq!(after["data"][TABLE], json!([{ "id": 1 }]), "{after}");
sqlx::query(&format!("DROP TABLE public.{TABLE}"))
.execute(&admin)
.await
.unwrap();
}
#[tokio::test]
#[ignore = "requires PostgreSQL"]
async fn a_schema_that_failed_at_start_up_is_built_on_reload() {
const SCHEMA: &str = "graphql_reload_broken";
const CHANNEL: &str = "graphql_reload_broken_test";
let admin = sqlx::PgPool::connect(&database_url()).await.unwrap();
for statement in [
format!("DROP SCHEMA IF EXISTS {SCHEMA} CASCADE"),
format!("CREATE SCHEMA {SCHEMA}"),
format!("GRANT USAGE ON SCHEMA {SCHEMA} TO web_anon"),
format!("CREATE TABLE {SCHEMA}.probe (id int PRIMARY KEY)"),
format!("INSERT INTO {SCHEMA}.probe VALUES (1)"),
format!("GRANT SELECT ON {SCHEMA}.probe TO web_anon"),
format!("CREATE TABLE {SCHEMA}.\"Int\" (id int PRIMARY KEY)"),
] {
sqlx::query(&statement).execute(&admin).await.unwrap();
}
let mut config = AppConfig::default();
config.db_uri = database_url();
config.db_schemas = vec![SCHEMA.to_string()];
config.db_anon_role = Some("web_anon".to_string());
config.db_channel_enabled = true;
config.db_channel = CHANNEL.to_string();
let app = build(config, Host::SERVER).await.expect("build").router;
let before = query(&app, "{ probe { id } }").await;
assert_eq!(
before["errors"][0]["extensions"]["code"], "unexpected",
"{before}"
);
sqlx::query(&format!("DROP TABLE {SCHEMA}.\"Int\""))
.execute(&admin)
.await
.unwrap();
let mut after = Value::Null;
for _ in 0..50 {
reload(&admin, CHANNEL).await;
after = query(&app, "{ probe { id } }").await;
if after["data"]["probe"] == json!([{ "id": 1 }]) {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
assert_eq!(after["data"]["probe"], json!([{ "id": 1 }]), "{after}");
sqlx::query(&format!("DROP SCHEMA {SCHEMA} CASCADE"))
.execute(&admin)
.await
.unwrap();
}