use api::{AppState, build_app}; use axum::body::Body; use axum::http::{Request, StatusCode}; use db::catalog; use domain::{AuditActor, ObjectInput, Visibility}; use http_body_util::BodyExt; use sqlx::PgPool; use tower::ServiceExt; // for `oneshot` fn state(pool: PgPool) -> AppState { AppState { db: db::Db::from_pool(pool), app_name: "Test".to_string(), } } fn object(number: &str, name: &str, visibility: Visibility) -> ObjectInput { ObjectInput { object_number: number.into(), object_name: name.into(), number_of_objects: 1, brief_description: Some("a description".into()), current_location: Some("vault B".into()), // never-public; must NOT appear in output current_owner: Some("the museum".into()), // never-public recorder: None, recording_date: None, visibility, } } async fn body_json(resp: axum::http::Response) -> serde_json::Value { let bytes = resp.into_body().collect().await.unwrap().to_bytes(); serde_json::from_slice(&bytes).unwrap() } #[sqlx::test(migrations = "../db/migrations")] async fn list_returns_only_public_as_public_view(pool: PgPool) { let db = db::Db::from_pool(pool.clone()); let mut tx = db.pool().begin().await.unwrap(); catalog::create_object( &mut tx, AuditActor::System, &object("D-1", "draft vase", Visibility::Draft), ) .await .unwrap(); catalog::create_object( &mut tx, AuditActor::System, &object("P-1", "public vase", Visibility::Public), ) .await .unwrap(); tx.commit().await.unwrap(); let app = build_app(state(pool)); let resp = app .oneshot( Request::builder() .uri("/api/public/objects") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let json = body_json(resp).await; assert_eq!(json["total"], 1); assert_eq!(json["items"].as_array().unwrap().len(), 1); let item = &json["items"][0]; assert_eq!(item["object_number"], "P-1"); assert_eq!(item["object_name"], "public vase"); assert_eq!(item["brief_description"], "a description"); assert!(item.get("current_location").is_none()); assert!(item.get("current_owner").is_none()); assert!(item.get("recorder").is_none()); assert!(item.get("visibility").is_none()); } #[sqlx::test(migrations = "../db/migrations")] async fn get_public_object_returns_it(pool: PgPool) { let db = db::Db::from_pool(pool.clone()); let mut tx = db.pool().begin().await.unwrap(); let id = catalog::create_object( &mut tx, AuditActor::System, &object("P-1", "public vase", Visibility::Public), ) .await .unwrap(); tx.commit().await.unwrap(); 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::OK); let json = body_json(resp).await; assert_eq!(json["object_number"], "P-1"); assert!(json.get("current_location").is_none()); } #[sqlx::test(migrations = "../db/migrations")] 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 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)); 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")] async fn get_missing_object_is_404(pool: PgPool) { let app = build_app(state(pool)); let resp = app .oneshot( Request::builder() .uri(format!("/api/public/objects/{}", domain::ObjectId::new())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[sqlx::test(migrations = "../db/migrations")] async fn openapi_lists_the_public_paths(pool: PgPool) { let app = build_app(state(pool)); let resp = app .oneshot( Request::builder() .uri("/api-docs/openapi.json") .body(Body::empty()) .unwrap(), ) .await .unwrap(); let json = body_json(resp).await; assert!(json["paths"]["/api/public/objects"].is_object()); assert!(json["paths"]["/api/public/objects/{id}"].is_object()); }