#![allow(clippy::unwrap_used)]
use std::sync::Arc;
use axum::body::Body;
use fraiseql_core::schema::CompiledSchema;
use fraiseql_test_utils::failing_adapter::FailingAdapter;
use http::{Request, StatusCode};
use tower::ServiceExt;
use crate::{server::Server, server_config::ServerConfig};
async fn server_with_query_method(enabled: bool) -> Server {
let config = ServerConfig {
cors_enabled: false,
enable_http_query: enabled,
..ServerConfig::default()
};
Box::pin(Server::new(
config,
CompiledSchema::new(),
Arc::new(FailingAdapter::new()),
None,
))
.await
.expect("Server::new should succeed for an empty schema")
}
async fn status_for(enabled: bool, method: &str, body: &str) -> StatusCode {
let server = server_with_query_method(enabled).await;
let state = server.build_app_state();
let app = server.build_graphql_router(&state);
let request = Request::builder()
.method(method)
.uri("/graphql")
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap();
app.oneshot(request).await.unwrap().status()
}
async fn get_status_for(enabled: bool, query: &str) -> StatusCode {
let server = server_with_query_method(enabled).await;
let state = server.build_app_state();
let app = server.build_graphql_router(&state);
let uri = format!("/graphql?query={}", urlencoding::encode(query));
let request = Request::builder().uri(uri).body(Body::empty()).unwrap();
app.oneshot(request).await.unwrap().status()
}
const QUERY_DOC: &str = r#"{"query":"query { users { id } }"}"#;
const MUTATION_DOC: &str = r#"{"query":"mutation { createUser(name: \"x\") { id } }"}"#;
const SUBSCRIPTION_DOC: &str = r#"{"query":"subscription { userAdded { id } }"}"#;
#[tokio::test]
async fn query_method_with_a_query_is_accepted_when_enabled() {
let status = status_for(true, "QUERY", QUERY_DOC).await;
assert_ne!(
status,
StatusCode::METHOD_NOT_ALLOWED,
"QUERY + query must dispatch into the POST execution path when enabled"
);
}
#[tokio::test]
async fn query_method_refuses_a_mutation() {
assert_eq!(
status_for(true, "QUERY", MUTATION_DOC).await,
StatusCode::METHOD_NOT_ALLOWED,
"a mutation over the safe, retryable QUERY method must be refused"
);
}
#[tokio::test]
async fn query_method_refuses_a_subscription() {
assert_eq!(
status_for(true, "QUERY", SUBSCRIPTION_DOC).await,
StatusCode::METHOD_NOT_ALLOWED,
"a subscription over QUERY must be refused"
);
}
#[tokio::test]
async fn query_method_is_refused_when_disabled() {
assert_eq!(
status_for(false, "QUERY", QUERY_DOC).await,
StatusCode::METHOD_NOT_ALLOWED,
"QUERY is opt-in: with the flag off the endpoint must not accept it"
);
}
#[tokio::test]
async fn an_unrelated_method_is_still_refused_when_query_is_enabled() {
assert_eq!(
status_for(true, "DELETE", QUERY_DOC).await,
StatusCode::METHOD_NOT_ALLOWED,
"enabling QUERY must not make the endpoint answer other methods"
);
}
#[tokio::test]
async fn post_and_get_are_unchanged_by_the_flag() {
for enabled in [false, true] {
assert_ne!(
status_for(enabled, "POST", QUERY_DOC).await,
StatusCode::METHOD_NOT_ALLOWED,
"POST must keep working with enable_http_query = {enabled}"
);
assert_ne!(
get_status_for(enabled, "query { users { id } }").await,
StatusCode::METHOD_NOT_ALLOWED,
"GET must keep working with enable_http_query = {enabled}"
);
assert_eq!(
get_status_for(enabled, "mutation { createUser(name: \"x\") { id } }").await,
StatusCode::METHOD_NOT_ALLOWED,
"GET must still refuse mutations with enable_http_query = {enabled}"
);
}
}