#![allow(clippy::unwrap_used)]
use std::{collections::HashMap, sync::Arc};
use axum::{Router, 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(config: ServerConfig) -> Server {
let config = ServerConfig {
cors_enabled: false,
..config
};
Box::pin(Server::new(
config,
CompiledSchema::new(),
Arc::new(FailingAdapter::new()),
None,
))
.await
.expect("Server::new should succeed for an empty schema + default config")
}
fn minimal_storage_state() -> fraiseql_storage::StorageState {
fraiseql_storage::StorageState::new(
Arc::new(fraiseql_storage::StorageBackend::Local(fraiseql_storage::LocalBackend::new(
"/tmp/fraiseql-c6-test-unused",
))),
Arc::new(fraiseql_storage::StorageMetadataRepo::new(
sqlx::PgPool::connect_lazy("postgres://test:test@localhost/test").unwrap(),
)),
fraiseql_storage::StorageRlsEvaluator::new(),
HashMap::new(),
Arc::new(fraiseql_storage::UploadSessionRepo::new(
sqlx::PgPool::connect_lazy("postgres://test:test@localhost/test").unwrap(),
)),
Arc::new(fraiseql_storage::StoragePolicyStore::new(
sqlx::PgPool::connect_lazy("postgres://test:test@localhost/test").unwrap(),
)),
)
}
#[tokio::test]
async fn hardened_storage_state_not_mounted_without_any_auth() {
let server = server_with(ServerConfig::default())
.await
.with_storage_state(minimal_storage_state());
let state = server.build_app_state();
let app: Router = server.mount_extensions(Router::new(), &state);
let response = app
.oneshot(
Request::builder()
.method("PATCH")
.uri("/storage/v1/object/secret.txt")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::NOT_FOUND,
"hardened storage API must fail closed when neither storage_token nor an OIDC validator is set",
);
}
#[tokio::test]
async fn storage_state_is_mounted_with_a_storage_token() {
let config = ServerConfig {
storage_token: Some("storage-admin-token-32chars-minimum".to_string()),
..ServerConfig::default()
};
let server = server_with(config).await.with_storage_state(minimal_storage_state());
let state = server.build_app_state();
let app: Router = server.mount_extensions(Router::new(), &state);
let response = app
.oneshot(
Request::builder()
.method("PATCH")
.uri("/storage/v1/object/docs/secret.txt")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::METHOD_NOT_ALLOWED,
"with a storage_token the storage routes must be mounted (405 for an unregistered \
method), not absent (404)",
);
}
#[cfg(feature = "mcp")]
mod mcp_host_check {
use super::*;
const INITIALIZE: &str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}"#;
async fn mounted_mcp(require_auth: bool) -> Router {
let mut schema = CompiledSchema::new();
schema.mcp_config = Some(fraiseql_core::schema::McpConfig {
enabled: true,
transport: "http".to_string(),
path: "/mcp".to_string(),
require_auth,
..fraiseql_core::schema::McpConfig::default()
});
std::env::set_var("FRAISEQL_TEST_MCP_HOST_HS256", "mcp-host-check-hs256-secret-32by!");
let config = ServerConfig {
cors_enabled: false,
auth_hs256: Some(crate::server_config::hs256::Hs256Config {
secret_env: "FRAISEQL_TEST_MCP_HOST_HS256".to_string(),
issuer: None,
audience: Some("fraiseql".to_string()),
}),
..ServerConfig::default()
};
let server = Box::pin(Server::new(config, schema, Arc::new(FailingAdapter::new()), None))
.await
.expect("Server::new with [mcp] and [auth_hs256]");
let state = server.build_app_state();
server.mount_extensions(Router::new(), &state)
}
async fn initialize_status(app: Router, host: &str) -> StatusCode {
let request = Request::post("/mcp")
.header("host", host)
.header("content-type", "application/json")
.header("accept", "application/json, text/event-stream")
.body(Body::from(INITIALIZE))
.unwrap();
app.oneshot(request).await.unwrap().status()
}
#[tokio::test]
async fn with_require_auth_the_deployments_hostname_is_served() {
let app = mounted_mcp(true).await;
for host in ["api.example.com", "tenant-a.example.com:8443"] {
assert_eq!(initialize_status(app.clone(), host).await, StatusCode::OK, "{host}");
}
}
#[tokio::test]
async fn without_require_auth_a_foreign_host_is_refused() {
let app = mounted_mcp(false).await;
assert_eq!(initialize_status(app.clone(), "localhost").await, StatusCode::OK);
assert_eq!(initialize_status(app, "attacker.example").await, StatusCode::FORBIDDEN);
}
}
mod api_v1_intelligence_routes {
use super::*;
const ADMIN_TOKEN: &str = "admin-token-for-the-mount-test-32chars";
fn paths() -> Vec<&'static str> {
let mut paths = vec!["/api/v1/query/stats"];
if cfg!(feature = "federation") {
paths.extend([
"/api/v1/federation/subgraphs",
"/api/v1/federation/graph",
"/api/v1/federation/plan?query=%7B__typename%7D",
]);
}
paths
}
async fn app(admin: bool) -> Router {
let config = if admin {
ServerConfig {
admin_api_enabled: true,
admin_token: Some(ADMIN_TOKEN.to_string()),
..ServerConfig::default()
}
} else {
ServerConfig::default()
};
let server = server_with(config).await;
let state = server.build_app_state();
server.build_router(&state)
}
async fn status(app: &Router, path: &str, bearer: Option<&str>) -> StatusCode {
let mut request = Request::builder().method("GET").uri(path);
if let Some(token) = bearer {
request = request.header("authorization", format!("Bearer {token}"));
}
app.clone()
.oneshot(request.body(Body::empty()).unwrap())
.await
.unwrap()
.status()
}
#[tokio::test]
async fn an_anonymous_caller_is_refused() {
let app = app(true).await;
for path in paths() {
assert_eq!(status(&app, path, None).await, StatusCode::UNAUTHORIZED, "{path}");
}
}
#[tokio::test]
async fn the_admin_read_token_is_served() {
let app = app(true).await;
for path in paths() {
let status = status(&app, path, Some(ADMIN_TOKEN)).await;
assert!(
status != StatusCode::UNAUTHORIZED && status != StatusCode::NOT_FOUND,
"{path}: {status}"
);
}
}
#[tokio::test]
async fn without_an_admin_token_they_are_not_mounted() {
let app = app(false).await;
for path in paths() {
assert_eq!(status(&app, path, None).await, StatusCode::NOT_FOUND, "{path}");
}
}
#[tokio::test]
async fn query_validate_is_not_mounted() {
let app = app(true).await;
let request = Request::builder()
.method("POST")
.uri("/api/v1/query/validate")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {ADMIN_TOKEN}"))
.body(Body::from(r#"{"query":"{ __typename }"}"#))
.unwrap();
let status = app.oneshot(request).await.unwrap().status();
assert_eq!(status, StatusCode::NOT_FOUND);
}
}
mod storage_revocation {
use super::*;
use crate::{
testing::idp::{TestIdp, token},
token_revocation::{InMemoryRevocationStore, RevocationStore, TokenRevocationManager},
};
async fn status(jti: &str, revoked: &str) -> StatusCode {
let idp = TestIdp::start().await;
let store = Arc::new(InMemoryRevocationStore::new());
store.revoke(revoked, 3600).await.unwrap();
let mut server = server_with(ServerConfig::default())
.await
.with_storage_state(minimal_storage_state())
.with_revocation_manager(Arc::new(TokenRevocationManager::new(
store, false, false, 3600,
)));
server.oidc_validator = Some(idp.validator(false));
let state = server.build_app_state();
let app: Router = server.mount_extensions(Router::new(), &state);
let request = Request::builder()
.method("GET")
.uri("/storage/v1/object/docs/report.txt")
.header("authorization", format!("Bearer {}", token("alice", jti)))
.body(Body::empty())
.unwrap();
app.oneshot(request).await.unwrap().status()
}
#[tokio::test]
async fn a_revoked_token_is_refused() {
assert_eq!(status("j-revoked", "j-revoked").await, StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn a_live_token_is_admitted() {
assert_ne!(status("j-live", "j-revoked").await, StatusCode::UNAUTHORIZED);
}
}