chore(api): drop unused uuid dep + redundant domain dev-dep; test internal exclusion + note list/count race
This commit is contained in:
@@ -10,7 +10,6 @@ serde.workspace = true
|
|||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
domain = { path = "../domain" }
|
domain = { path = "../domain" }
|
||||||
uuid.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
@@ -18,4 +17,3 @@ tower.workspace = true
|
|||||||
http-body-util.workspace = true
|
http-body-util.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
domain = { path = "../domain" }
|
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ pub(crate) async fn list_objects(
|
|||||||
) -> Result<Json<PublicObjectPage>, StatusCode> {
|
) -> Result<Json<PublicObjectPage>, StatusCode> {
|
||||||
let (limit, offset) = (page.limit(), page.offset());
|
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)
|
let objects = db::catalog::list_public_objects(state.db.pool(), limit, offset)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|||||||
@@ -108,20 +108,30 @@ async fn get_public_object_returns_it(pool: PgPool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrations = "../db/migrations")]
|
#[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 db = db::Db::from_pool(pool.clone());
|
||||||
let mut tx = db.pool().begin().await.unwrap();
|
let mut tx = db.pool().begin().await.unwrap();
|
||||||
let id = catalog::create_object(
|
let draft = catalog::create_object(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
AuditActor::System,
|
AuditActor::System,
|
||||||
&object("D-1", "draft vase", Visibility::Draft),
|
&object("D-1", "draft vase", Visibility::Draft),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let internal = catalog::create_object(
|
||||||
|
&mut tx,
|
||||||
|
AuditActor::System,
|
||||||
|
&object("I-1", "internal vase", Visibility::Internal),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
tx.commit().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 app = build_app(state(pool));
|
||||||
|
for id in [draft, internal] {
|
||||||
let resp = app
|
let resp = app
|
||||||
|
.clone()
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.uri(format!("/api/public/objects/{id}"))
|
.uri(format!("/api/public/objects/{id}"))
|
||||||
@@ -130,7 +140,8 @@ async fn get_non_public_object_is_404(pool: PgPool) {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND); // not 403 — don't leak existence
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrations = "../db/migrations")]
|
#[sqlx::test(migrations = "../db/migrations")]
|
||||||
|
|||||||
Reference in New Issue
Block a user