use std::sync::Arc;
use axum::body::{to_bytes, Body};
use axum::http::{Request, StatusCode};
use sbol_db_app::AppServices;
use sbol_db_postgres::{connect, run_migrations, JobRepository, SbolObjectService};
use sbol_db_server::{router, AppState, Metrics, SchemaCache, ServerConfig};
use sbol_db_sparql::{SparqlEngine, SparqlUpdateEngine};
use tower::ServiceExt;
const BODY_LIMIT: usize = 1024 * 1024;
async fn state() -> AppState {
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://sbol:sbol@localhost:5432/sbol".to_owned());
let pool = connect(&database_url).await.expect("connect");
run_migrations(&pool).await.expect("migrate");
let service = Arc::new(SbolObjectService::new(pool.clone()));
let sparql = Arc::new(SparqlEngine::new(service.triple_source()));
let sparql_update = Arc::new(SparqlUpdateEngine::new(
service.triple_source(),
service.triple_writer(),
));
let jobs = Arc::new(JobRepository::new(pool.clone()));
let pool_console = pool.clone();
let pool_stats = pool.clone();
let metrics = Metrics::install(Some(pool.clone()), env!("CARGO_PKG_VERSION"))
.with_worker_pool(pool)
.with_jobs_repo(jobs.clone());
AppState {
lab: service.clone(),
app: Arc::new(AppServices::new(
service.clone(),
sparql.clone(),
sparql_update.clone(),
jobs.clone(),
service.clone(),
)),
service,
sparql,
sparql_update,
metrics,
jobs,
config: ServerConfig::default(),
backend_kind: sbol_db_server::BackendKind::Postgres,
sql_console: Some(Arc::new(sbol_db_postgres::PgSqlConsole::new(pool_console))),
db_stats: Some(Arc::new(sbol_db_postgres::PgStatsRepository::new(
pool_stats,
))),
lsm_stats: None,
schema_cache: Arc::new(SchemaCache::new()),
}
}
async fn read_body(res: axum::response::Response) -> String {
let bytes = to_bytes(res.into_body(), BODY_LIMIT).await.expect("body");
String::from_utf8(bytes.to_vec()).expect("utf8")
}
async fn send(req: Request<Body>) -> axum::response::Response {
let app = router(state().await, ServerConfig::default());
app.oneshot(req).await.expect("request")
}
#[tokio::test]
async fn create_document_without_format_or_content_type_is_400() {
let res = send(
Request::builder()
.method("POST")
.uri("/graphs")
.body(Body::from("not really turtle"))
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body = read_body(res).await;
assert!(
body.contains("format"),
"body should reference format: {body}"
);
}
#[tokio::test]
async fn create_document_with_unknown_format_is_400() {
let res = send(
Request::builder()
.method("POST")
.uri("/graphs?format=parquet")
.body(Body::from(""))
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body = read_body(res).await;
assert!(body.contains("unknown format"), "got {body}");
}
#[tokio::test]
async fn create_document_with_invalid_iri_is_400() {
let res = send(
Request::builder()
.method("POST")
.uri("/graphs?format=turtle&document_iri=not-an-iri")
.header("content-type", "text/turtle")
.body(Body::from("# empty"))
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn sparql_update_query_is_rejected_400() {
let update = "INSERT DATA { <https://x> <https://p> <https://o> }";
let res = send(
Request::builder()
.method("POST")
.uri("/sparql")
.header("content-type", "application/sparql-query")
.body(Body::from(update))
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body = read_body(res).await;
assert!(
body.contains("sparql_update_not_allowed") || body.to_lowercase().contains("update"),
"expected sparql_update_not_allowed error code, got {body}"
);
}
#[tokio::test]
async fn sparql_get_without_query_param_is_400() {
let res = send(
Request::builder()
.uri("/sparql")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn sparql_malformed_query_is_400() {
let res = send(
Request::builder()
.method("POST")
.uri("/sparql")
.header("content-type", "application/sparql-query")
.body(Body::from("SELECT ?s WHERE { ?s ?p"))
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn neighborhood_with_invalid_iri_is_400() {
let res = send(
Request::builder()
.uri("/objects/neighborhood?iri=not-an-iri")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn neighborhood_with_unknown_direction_is_400() {
let res = send(
Request::builder()
.uri("/objects/neighborhood?iri=https://example.org/x&direction=sideways")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body = read_body(res).await;
assert!(body.contains("direction"), "got {body}");
}
#[tokio::test]
async fn neighborhood_rdf_with_unknown_format_is_400() {
let res = send(
Request::builder()
.uri("/objects/neighborhood.rdf?iri=https://example.org/x&format=yaml")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body = read_body(res).await;
assert!(body.contains("unknown format"), "got {body}");
}
#[tokio::test]
async fn objects_lookup_with_missing_iri_is_400() {
let res = send(
Request::builder()
.uri("/objects")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn get_document_with_malformed_uuid_is_400_or_404() {
let res = send(
Request::builder()
.uri("/graphs/not-a-uuid")
.body(Body::empty())
.unwrap(),
)
.await;
assert!(
matches!(
res.status(),
StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND
),
"expected 400/404 for malformed UUID, got {}",
res.status()
);
}
#[tokio::test]
async fn get_document_with_unknown_uuid_is_404() {
let res = send(
Request::builder()
.uri("/graphs/00000000-0000-0000-0000-000000000000")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn get_job_with_unknown_uuid_is_404() {
let res = send(
Request::builder()
.uri("/jobs/00000000-0000-0000-0000-000000000000")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn sequence_search_without_pattern_is_400() {
let res = send(
Request::builder()
.uri("/sequences/search")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn unknown_route_returns_404_with_json_body() {
let res = send(
Request::builder()
.uri("/this/route/does/not/exist")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let body = read_body(res).await;
assert!(body.contains("\"not_found\""), "got {body}");
}