#![cfg(feature = "server")]
use axum::{
body::Body,
http::{Request, StatusCode},
};
use http_body_util::BodyExt;
use oxibase::api::Database;
use oxibase::server::create_router;
use serde_json::{json, Value};
use tower::ServiceExt;
async fn setup_db(dsn: &str) -> Database {
let db = Database::open(dsn).unwrap();
db.execute("CREATE TABLE IF NOT EXISTS users (id INT, name TEXT)", ())
.unwrap();
db
}
async fn get_json_response(response: axum::response::Response) -> Value {
let body = response.into_body().collect().await.unwrap().to_bytes();
serde_json::from_slice(&body).unwrap()
}
#[tokio::test]
async fn test_auto_api_crud_flow() {
let db = setup_db("memory://test_auto_api_crud_flow").await;
let app = create_router(db);
let req = Request::builder()
.uri("/api/data/users")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = get_json_response(res).await;
assert_eq!(body, json!([]));
let req = Request::builder()
.method("POST")
.uri("/api/data/users")
.header("content-type", "application/json")
.body(Body::from(json!({ "id": 1, "name": "Alice" }).to_string()))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::CREATED);
let body = get_json_response(res).await;
assert_eq!(body, json!({ "rows_affected": 1 }));
let req = Request::builder()
.uri("/api/data/users")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = get_json_response(res).await;
assert_eq!(body, json!([{"id": 1, "name": "Alice"}]));
let req = Request::builder()
.method("PATCH")
.uri("/api/data/users?id=eq.1")
.header("content-type", "application/json")
.body(Body::from(json!({ "name": "Alice Bob" }).to_string()))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = get_json_response(res).await;
assert_eq!(body, json!({ "rows_affected": 1 }));
let req = Request::builder()
.uri("/api/data/users?id=eq.1")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = get_json_response(res).await;
assert_eq!(body, json!([{"id": 1, "name": "Alice Bob"}]));
let req = Request::builder()
.method("DELETE")
.uri("/api/data/users?id=eq.1")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = get_json_response(res).await;
assert_eq!(body, json!({ "rows_affected": 1 }));
let req = Request::builder()
.uri("/api/data/users")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = get_json_response(res).await;
assert_eq!(body, json!([]));
}
#[tokio::test]
async fn test_auto_api_edge_cases_and_errors() {
let db = Database::open("memory://test_auto_api_edge_cases").unwrap();
db.execute("CREATE TABLE complex_types (id INT, is_active BOOLEAN, score FLOAT, tags JSON, label TEXT)", ())
.unwrap();
let app = create_router(db);
let req = Request::builder()
.uri("/api/data/nope")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = Request::builder()
.method("POST")
.uri("/api/data/nope")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = Request::builder()
.method("PATCH")
.uri("/api/data/nope?id=eq.1")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = Request::builder()
.method("DELETE")
.uri("/api/data/nope?id=eq.1")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = Request::builder()
.method("POST")
.uri("/api/data/complex_types")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let req = Request::builder()
.method("PATCH")
.uri("/api/data/complex_types?id=eq.1")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let req = Request::builder()
.method("PATCH")
.uri("/api/data/complex_types")
.header("content-type", "application/json")
.body(Body::from(json!({"label": "foo"}).to_string()))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let req = Request::builder()
.method("DELETE")
.uri("/api/data/complex_types")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let req = Request::builder()
.method("POST")
.uri("/api/data/complex_types")
.header("content-type", "application/json")
.body(Body::from(
json!({
"id": 1,
"is_active": true,
"score": 42.5,
"tags": ["a", "b"],
"label": null
})
.to_string(),
))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::CREATED);
let req = Request::builder()
.uri("/api/data/complex_types")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let body = get_json_response(res).await;
assert_eq!(body[0]["is_active"], true);
assert_eq!(body[0]["score"], 42.5);
assert_eq!(body[0]["tags"], "[\"a\",\"b\"]"); assert_eq!(body[0]["label"], Value::Null);
let req = Request::builder()
.uri("/api/data/complex_types?select=bad_col")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.method("POST")
.uri("/api/data/complex_types")
.header("content-type", "application/json")
.body(Body::from(json!({"id": "not_an_int"}).to_string()))
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn test_dynamic_route_rendering() {
let db = setup_db("memory://test_dynamic_route_rendering").await;
let app = create_router(db.clone());
db.execute("INSERT INTO interface.templates (name, content) VALUES ('hello.html', '<h1>Hello {{ data[0].name }}!</h1>')", ()).unwrap();
db.execute("INSERT INTO interface.routes (method, path, template_name, context_query) VALUES ('GET', '/hello', 'hello.html', 'SELECT ''World'' AS name')", ()).unwrap();
let req = Request::builder()
.uri("/hello")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("<h1>Hello World!</h1>") || body_str.contains("Hello"),
"Body was: {}",
body_str
);
}
#[tokio::test]
async fn test_dynamic_route_updates() {
let db = setup_db("memory://test_dynamic_route_updates").await;
let app = create_router(db.clone());
db.execute(
"INSERT INTO interface.templates (name, content) VALUES ('page.html', '<h1>Version 1</h1>')",
(),
)
.unwrap();
db.execute("INSERT INTO interface.routes (method, path, template_name) VALUES ('GET', '/page', 'page.html')", ()).unwrap();
let req = Request::builder().uri("/page").body(Body::empty()).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("Version 1"));
db.execute(
"UPDATE interface.templates SET content = '<h1>Version 2</h1>' WHERE name = 'page.html'",
(),
)
.unwrap();
let req2 = Request::builder().uri("/page").body(Body::empty()).unwrap();
let res2 = app.clone().oneshot(req2).await.unwrap();
let body2 = res2.into_body().collect().await.unwrap().to_bytes();
let body_str2 = String::from_utf8(body2.to_vec()).unwrap();
assert!(body_str2.contains("Version 2"));
db.execute("DELETE FROM interface.routes WHERE path = '/page'", ())
.unwrap();
let req3 = Request::builder().uri("/page").body(Body::empty()).unwrap();
let res3 = app.clone().oneshot(req3).await.unwrap();
assert_eq!(res3.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_dynamic_route_template_inheritance() {
let db = setup_db("memory://test_dynamic_route_template_inheritance").await;
let app = create_router(db.clone());
db.execute(
"INSERT INTO interface.templates (name, content) VALUES ('layout.html', '<main>{% block content %}{% endblock %}</main>')",
(),
)
.unwrap();
db.execute(
"INSERT INTO interface.templates (name, content) VALUES ('child.html', '{% extends \"layout.html\" %}{% block content %}<p>Inherited Content!</p>{% endblock %}')",
(),
)
.unwrap();
db.execute("INSERT INTO interface.routes (method, path, template_name) VALUES ('GET', '/inherited', 'child.html')", ()).unwrap();
let req = Request::builder()
.uri("/inherited")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = res.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("<main><p>Inherited Content!</p></main>"));
}