From 720c7ddbbfdcf32d500a92e8c456f9e407f7c4c9 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Tue, 2 Jun 2026 13:55:01 +0200 Subject: [PATCH] chore(api): drop unused uuid dep + redundant domain dev-dep; test internal exclusion + note list/count race --- crates/api/Cargo.toml | 2 -- crates/api/src/public.rs | 3 +++ crates/api/tests/public.rs | 35 +++++++++++++++++++++++------------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 742ce8f..5eda5af 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -10,7 +10,6 @@ serde.workspace = true utoipa.workspace = true db = { path = "../db" } domain = { path = "../domain" } -uuid.workspace = true [dev-dependencies] tokio.workspace = true @@ -18,4 +17,3 @@ tower.workspace = true http-body-util.workspace = true serde_json.workspace = true sqlx.workspace = true -domain = { path = "../domain" } diff --git a/crates/api/src/public.rs b/crates/api/src/public.rs index 185d62d..3c61db5 100644 --- a/crates/api/src/public.rs +++ b/crates/api/src/public.rs @@ -86,6 +86,9 @@ pub(crate) async fn list_objects( ) -> Result, StatusCode> { let (limit, offset) = (page.limit(), page.offset()); + // `items` and `total` come from two separate queries; under concurrent + // publish/unpublish they can momentarily disagree by one — acceptable for a + // public read surface. let objects = db::catalog::list_public_objects(state.db.pool(), limit, offset) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; diff --git a/crates/api/tests/public.rs b/crates/api/tests/public.rs index 3aa8720..a3a3c0e 100644 --- a/crates/api/tests/public.rs +++ b/crates/api/tests/public.rs @@ -108,29 +108,40 @@ async fn get_public_object_returns_it(pool: PgPool) { } #[sqlx::test(migrations = "../db/migrations")] -async fn get_non_public_object_is_404(pool: PgPool) { +async fn non_public_objects_are_404(pool: PgPool) { let db = db::Db::from_pool(pool.clone()); let mut tx = db.pool().begin().await.unwrap(); - let id = catalog::create_object( + let draft = catalog::create_object( &mut tx, AuditActor::System, &object("D-1", "draft vase", Visibility::Draft), ) .await .unwrap(); + let internal = catalog::create_object( + &mut tx, + AuditActor::System, + &object("I-1", "internal vase", Visibility::Internal), + ) + .await + .unwrap(); tx.commit().await.unwrap(); + // both non-public states are hidden behind a 404 — not 403 — so existence isn't leaked let app = build_app(state(pool)); - let resp = app - .oneshot( - Request::builder() - .uri(format!("/api/public/objects/{id}")) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); // not 403 — don't leak existence + for id in [draft, internal] { + let resp = app + .clone() + .oneshot( + Request::builder() + .uri(format!("/api/public/objects/{id}")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } } #[sqlx::test(migrations = "../db/migrations")]