#![allow(clippy::unwrap_used)]
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
use axum::{Router, routing::get};
use fraiseql_storage::{
StorageMetadataRepo, StorageRlsEvaluator, StorageState,
backend::{LocalBackend, StorageBackend},
config::{BucketAccess, BucketConfig},
};
use futures::future::BoxFuture;
use sqlx::PgPool;
use tempfile::tempdir;
use tokio::net::TcpListener;
use crate::realtime::{
routes::realtime_router,
server::{RealtimeConfig, RealtimeServer, RealtimeState, TokenInfo, TokenValidator},
};
async fn spawn(router: Router) -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, router.into_make_service()).await.unwrap();
});
addr
}
#[derive(Clone)]
struct AlwaysOkValidator;
impl TokenValidator for AlwaysOkValidator {
fn validate<'a>(&'a self, _token: &'a str) -> BoxFuture<'a, Result<TokenInfo, String>> {
Box::pin(async move {
Ok(TokenInfo {
user_id: "test-user".to_string(),
context_hash: 0,
expires_at: i64::MAX,
})
})
}
}
fn realtime_state() -> RealtimeState {
let server = Arc::new(RealtimeServer::with_entities(
RealtimeConfig::default(),
["TestEntity".to_string()].into(),
));
RealtimeState {
server,
validator: Arc::new(AlwaysOkValidator),
}
}
async fn storage_state(bucket: &str) -> StorageState {
let tmp = tempdir().unwrap();
let backend = StorageBackend::Local(LocalBackend::new(tmp.path().to_str().unwrap()));
let mut buckets = HashMap::new();
buckets.insert(
bucket.to_string(),
BucketConfig {
name: bucket.to_string(),
max_object_bytes: None,
allowed_mime_types: None,
access: BucketAccess::PublicRead,
transform_presets: None,
serve_inline: false,
},
);
StorageState {
backend: Arc::new(backend),
metadata: Arc::new(StorageMetadataRepo::new(lazy_pool())),
rls: StorageRlsEvaluator::new(),
buckets: Arc::new(buckets),
}
}
fn lazy_pool() -> PgPool {
PgPool::connect_lazy("postgres://test:test@localhost/test").unwrap()
}
#[tokio::test]
async fn test_realtime_route_mounted_when_enabled() {
let router = Router::new()
.route("/health", get(|| async { "ok" }))
.merge(realtime_router(realtime_state()));
let addr = spawn(router).await;
let status = reqwest::get(format!("http://{addr}/realtime/v1?token=test"))
.await
.unwrap()
.status();
assert_ne!(status.as_u16(), 404, "/realtime/v1 should be mounted (got {status})");
}
#[tokio::test]
async fn test_realtime_route_not_mounted_when_disabled() {
let router = Router::new().route("/health", get(|| async { "ok" }));
let addr = spawn(router).await;
let status = reqwest::get(format!("http://{addr}/realtime/v1")).await.unwrap().status();
assert_eq!(status.as_u16(), 404, "/realtime/v1 should not be mounted");
}
#[tokio::test]
async fn test_storage_routes_mounted_when_enabled() {
let state = storage_state("public-test").await;
let router = Router::new()
.route("/health", get(|| async { "ok" }))
.merge(fraiseql_storage::storage_router(state));
let addr = spawn(router).await;
let status = reqwest::get(format!("http://{addr}/storage/v1/list/public-test"))
.await
.unwrap()
.status();
assert_ne!(status.as_u16(), 404, "/storage/v1/list should be mounted (got {status})");
}
#[tokio::test]
async fn test_storage_routes_not_mounted_when_disabled() {
let router = Router::new().route("/health", get(|| async { "ok" }));
let addr = spawn(router).await;
let status = reqwest::get(format!("http://{addr}/storage/v1/list/public-test"))
.await
.unwrap()
.status();
assert_eq!(status.as_u16(), 404, "/storage/v1 routes should not be mounted");
}
#[tokio::test]
async fn test_existing_routes_unaffected_when_subsystems_added() {
let rt_state = realtime_state();
let st_state = storage_state("coexist-test").await;
let router = Router::new()
.route("/health", get(|| async { "ok" }))
.merge(realtime_router(rt_state))
.merge(fraiseql_storage::storage_router(st_state));
let addr = spawn(router).await;
let health_status = reqwest::get(format!("http://{addr}/health")).await.unwrap().status();
assert_eq!(health_status.as_u16(), 200);
let rt_status = reqwest::get(format!("http://{addr}/realtime/v1?token=test"))
.await
.unwrap()
.status();
assert_ne!(rt_status.as_u16(), 404);
let st_status = reqwest::get(format!("http://{addr}/storage/v1/list/coexist-test"))
.await
.unwrap()
.status();
assert_ne!(st_status.as_u16(), 404);
}
#[cfg(feature = "observers")]
#[tokio::test]
async fn test_observer_runtime_routes_nested_under_api_prefix() {
use tokio::sync::RwLock;
use crate::{
observers::{
DlqState, RuntimeHealthState, observer_dlq_routes, observer_runtime_routes,
runtime::{ObserverRuntime, ObserverRuntimeConfig},
},
server::routing::observers::mount_observer_runtime_routes,
};
let runtime =
Arc::new(RwLock::new(ObserverRuntime::new(ObserverRuntimeConfig::new(lazy_pool()))));
let router = mount_observer_runtime_routes(
Router::new().route("/health", get(|| async { "ok" })),
observer_runtime_routes(RuntimeHealthState {
runtime: Arc::clone(&runtime),
}),
observer_dlq_routes(DlqState { runtime }),
);
let addr = spawn(router).await;
let nested = reqwest::get(format!("http://{addr}/api/observers/runtime/health"))
.await
.unwrap()
.status();
assert_ne!(
nested.as_u16(),
404,
"/api/observers/runtime/health should be mounted (got {nested})"
);
let dlq = reqwest::get(format!("http://{addr}/api/observers/dlq")).await.unwrap().status();
assert_ne!(dlq.as_u16(), 404, "/api/observers/dlq should be mounted (got {dlq})");
let root = reqwest::get(format!("http://{addr}/runtime/health")).await.unwrap().status();
assert_eq!(root.as_u16(), 404, "/runtime/health must not be mounted at the root");
}