use cratestack::axum::body::{Body, to_bytes};
use cratestack::axum::http::{Request, StatusCode};
use cratestack::include_server_schema;
use cratestack::sqlx::query;
use cratestack::{AuthProvider, CoolContext, CoolError, RequestContext, Value};
use cratestack_codec_cbor::CborCodec;
use cratestack_codec_json::JsonCodec;
use tower::util::ServiceExt;
include_server_schema!(
"tests/fixtures/nullable_relation_projection.cstack",
db = Postgres
);
mod support;
use support::pg;
async fn reset_schema(pool: &cratestack::sqlx::PgPool) {
query("DROP TABLE IF EXISTS proj_widgets, proj_widget_owners")
.execute(pool)
.await
.expect("drop tables");
query("CREATE TABLE proj_widget_owners (id BIGINT PRIMARY KEY, name TEXT NOT NULL)")
.execute(pool)
.await
.expect("create proj_widget_owners");
query(
"CREATE TABLE proj_widgets (
id BIGINT PRIMARY KEY,
label TEXT NOT NULL,
owner_id BIGINT
)",
)
.execute(pool)
.await
.expect("create proj_widgets");
}
async fn seed(pool: &cratestack::sqlx::PgPool) {
query("INSERT INTO proj_widget_owners (id, name) VALUES (1, 'Alice')")
.execute(pool)
.await
.expect("seed owner");
query("INSERT INTO proj_widgets (id, label, owner_id) VALUES (1, 'owned', 1), (2, 'orphan', NULL)")
.execute(pool)
.await
.expect("seed widgets");
}
#[derive(Clone)]
struct PassThroughAuth;
impl AuthProvider for PassThroughAuth {
type Error = CoolError;
fn authenticate(
&self,
_request: &RequestContext<'_>,
) -> impl core::future::Future<Output = Result<CoolContext, Self::Error>> + Send {
core::future::ready(Ok(CoolContext::authenticated([(
"id".to_owned(),
Value::Int(1),
)])))
}
}
async fn assert_null_include_round_trips<C>(pool: &cratestack::sqlx::PgPool, codec: C)
where
C: cratestack::HttpTransport + Clone + cratestack::CoolCodec,
{
let cool = cratestack_schema::Cratestack::builder(pool.clone()).build();
let router = cratestack_schema::axum::model_router(cool, codec.clone(), PassThroughAuth);
let response = router
.oneshot(
Request::get("/proj_widgets?include=owner&sort=id")
.header("accept", C::CONTENT_TYPE)
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("response body should read");
let widgets: Vec<cratestack::serde_json::Value> = codec
.decode(&body)
.expect("include=owner response must decode under this codec");
let owned = widgets[0]
.as_object()
.expect("first widget should be an object");
assert_eq!(
owned.get("label"),
Some(&cratestack::serde_json::Value::from("owned"))
);
let owner = owned
.get("owner")
.and_then(|value| value.as_object())
.expect("owned widget's owner include should be a present object");
assert_eq!(
owner.get("name"),
Some(&cratestack::serde_json::Value::from("Alice"))
);
let orphan = widgets[1]
.as_object()
.expect("second widget should be an object");
assert_eq!(
orphan.get("label"),
Some(&cratestack::serde_json::Value::from("orphan"))
);
assert_eq!(
orphan.get("owner"),
Some(&cratestack::serde_json::Value::Null),
"orphan widget's null owner include must round-trip as an explicit wire null, got: {:?}",
orphan.get("owner"),
);
}
#[tokio::test]
async fn nullable_to_one_relation_include_round_trips_as_null_over_cbor() {
let _guard = pg::serial_guard().await;
let Some(test_pg) = pg::connect_or_skip().await else {
return;
};
let pool = &test_pg.pool;
reset_schema(pool).await;
seed(pool).await;
assert_null_include_round_trips(pool, CborCodec).await;
}
#[tokio::test]
async fn nullable_to_one_relation_include_round_trips_as_null_over_json() {
let _guard = pg::serial_guard().await;
let Some(test_pg) = pg::connect_or_skip().await else {
return;
};
let pool = &test_pg.pool;
reset_schema(pool).await;
seed(pool).await;
assert_null_include_round_trips(pool, JsonCodec).await;
}