use api::{AppState, build_app, migrate_sessions}; use axum::body::Body; use axum::http::{Request, StatusCode, header}; use db::{audit, users}; use domain::{AuditAction, AuditActor, Email, NewUser, Role}; use http_body_util::BodyExt; use sqlx::PgPool; use tower::ServiceExt; fn state(pool: PgPool) -> AppState { AppState { db: db::Db::from_pool(pool), app_name: "Test".into(), cookie_secure: false, search: None, default_language: "sv".into(), default_timezone: "Europe/Stockholm".into(), } } async fn seed_user(pool: &PgPool, email: &str, password: &str, role: Role) { let db = db::Db::from_pool(pool.clone()); let mut tx = db.pool().begin().await.unwrap(); users::create_user( &mut tx, AuditActor::System, &NewUser { email: Email::parse(email).unwrap(), password_hash: auth::hash_password(password).unwrap(), role, }, ) .await .unwrap(); tx.commit().await.unwrap(); } fn login_request(email: &str, password: &str) -> Request { Request::builder() .method("POST") .uri("/api/admin/login") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(format!( r#"{{"email":"{email}","password":"{password}"}}"# ))) .unwrap() } fn session_cookie(resp: &axum::http::Response) -> String { resp.headers() .get(header::SET_COOKIE) .unwrap() .to_str() .unwrap() .split(';') .next() .unwrap() .to_owned() } async fn login(app: &axum::Router, email: &str, pw: &str) -> String { let resp = app.clone().oneshot(login_request(email, pw)).await.unwrap(); assert_eq!(resp.status(), StatusCode::NO_CONTENT); session_cookie(&resp) } #[sqlx::test(migrations = "../db/migrations")] async fn create_list_vocabulary_and_terms(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; // create a vocabulary let created = app .clone() .oneshot( Request::builder() .method("POST") .uri("/api/admin/vocabularies") .header(header::COOKIE, &cookie) .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"key":"colour"}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(created.status(), StatusCode::CREATED); let vocab: serde_json::Value = serde_json::from_slice(&created.into_body().collect().await.unwrap().to_bytes()).unwrap(); let vocab_id = vocab["id"].as_str().unwrap().to_owned(); // list vocabularies includes it let list = app .clone() .oneshot( Request::builder() .uri("/api/admin/vocabularies") .header(header::COOKIE, &cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); let list_json: serde_json::Value = serde_json::from_slice(&list.into_body().collect().await.unwrap().to_bytes()).unwrap(); assert!( list_json .as_array() .unwrap() .iter() .any(|item| item["key"] == "colour") ); // add a term with labels let term = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/admin/vocabularies/{vocab_id}/terms")) .header(header::COOKIE, &cookie) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( r#"{"external_uri":null,"labels":[{"lang":"en","label":"red"},{"lang":"sv","label":"röd"}]}"#, )) .unwrap(), ) .await .unwrap(); assert_eq!(term.status(), StatusCode::CREATED); // list terms shows it (with both labels) let terms = app .oneshot( Request::builder() .uri(format!("/api/admin/vocabularies/{vocab_id}/terms")) .header(header::COOKIE, &cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); let terms_json: serde_json::Value = serde_json::from_slice(&terms.into_body().collect().await.unwrap().to_bytes()).unwrap(); let arr = terms_json.as_array().unwrap(); assert_eq!(arr.len(), 1); assert_eq!(arr[0]["labels"].as_array().unwrap().len(), 2); } #[sqlx::test(migrations = "../db/migrations")] async fn vocabulary_create_requires_auth(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); let app = build_app(state(pool)); let resp = app .oneshot( Request::builder() .method("POST") .uri("/api/admin/vocabularies") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"key":"x"}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } async fn app2_get(app: &axum::Router, cookie: &str, uri: &str) -> StatusCode { app.clone() .oneshot( Request::builder() .uri(uri) .header(header::COOKIE, cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap() .status() } #[sqlx::test(migrations = "../db/migrations")] async fn create_and_list_authorities_by_kind(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let created = app .clone() .oneshot( Request::builder() .method("POST") .uri("/api/admin/authorities") .header(header::COOKIE, &cookie) .header(header::CONTENT_TYPE, "application/json") .body(Body::from( r#"{"kind":"person","external_uri":null,"labels":[{"lang":"en","label":"Ada Lovelace"}]}"#, )) .unwrap(), ) .await .unwrap(); assert_eq!(created.status(), StatusCode::CREATED); // list by kind let list = app .clone() .oneshot( Request::builder() .uri("/api/admin/authorities?kind=person") .header(header::COOKIE, &cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(list.status(), StatusCode::OK); let json: serde_json::Value = serde_json::from_slice(&list.into_body().collect().await.unwrap().to_bytes()).unwrap(); assert_eq!(json.as_array().unwrap().len(), 1); assert_eq!(json[0]["kind"], "person"); // a different kind is empty let places = app .clone() .oneshot( Request::builder() .uri("/api/admin/authorities?kind=place") .header(header::COOKIE, &cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(places.status(), StatusCode::OK); let places_json: serde_json::Value = serde_json::from_slice(&places.into_body().collect().await.unwrap().to_bytes()).unwrap(); assert!(places_json.as_array().unwrap().is_empty()); // bad kind → 422 let bad = app2_get(&app, &cookie, "/api/admin/authorities?kind=alien").await; assert_eq!(bad, StatusCode::UNPROCESSABLE_ENTITY); } #[sqlx::test(migrations = "../db/migrations")] async fn add_term_to_missing_vocabulary_is_404(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let resp = app .oneshot( Request::builder() .method("POST") .uri("/api/admin/vocabularies/00000000-0000-0000-0000-000000000000/terms") .header(header::COOKIE, &cookie) .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"labels":[{"lang":"en","label":"X"}]}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[sqlx::test(migrations = "../db/migrations")] async fn creating_a_vocabulary_writes_an_audit_entry(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool.clone())); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let resp = app .oneshot( Request::builder() .method("POST") .uri("/api/admin/vocabularies") .header(header::COOKIE, &cookie) .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"key":"audit-test"}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); let body: serde_json::Value = serde_json::from_slice(&resp.into_body().collect().await.unwrap().to_bytes()).unwrap(); let vocab_id: uuid::Uuid = body["id"].as_str().unwrap().parse().unwrap(); let history = audit::history_for(&pool, "vocabulary", vocab_id) .await .unwrap(); assert_eq!(history.len(), 1); assert_eq!(history[0].action, AuditAction::Created); assert!( matches!(history[0].actor, AuditActor::User(_)), "expected actor to be a user" ); } async fn send( app: &axum::Router, cookie: &str, method: &str, uri: &str, body: Option<&str>, ) -> axum::http::Response { let mut req = Request::builder() .method(method) .uri(uri) .header(header::COOKIE, cookie); if body.is_some() { req = req.header(header::CONTENT_TYPE, "application/json"); } let body = body .map(|b| Body::from(b.to_owned())) .unwrap_or_else(Body::empty); app.clone().oneshot(req.body(body).unwrap()).await.unwrap() } #[sqlx::test(migrations = "../db/migrations")] async fn edit_and_delete_term(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let v = send( &app, &cookie, "POST", "/api/admin/vocabularies", Some(r#"{"key":"material"}"#), ) .await; let vid: serde_json::Value = serde_json::from_slice(&v.into_body().collect().await.unwrap().to_bytes()).unwrap(); let vid = vid["id"].as_str().unwrap().to_owned(); let t = send( &app, &cookie, "POST", &format!("/api/admin/vocabularies/{vid}/terms"), Some(r#"{"external_uri":null,"labels":[{"lang":"sv","label":"Trä"}]}"#), ) .await; let tid: serde_json::Value = serde_json::from_slice(&t.into_body().collect().await.unwrap().to_bytes()).unwrap(); let tid = tid["id"].as_str().unwrap().to_owned(); let patched = send( &app, &cookie, "PATCH", &format!("/api/admin/vocabularies/{vid}/terms/{tid}"), Some(r#"{"external_uri":"https://x","labels":[{"lang":"sv","label":"Träslag"}]}"#), ) .await; assert_eq!(patched.status(), StatusCode::NO_CONTENT); let deleted = send( &app, &cookie, "DELETE", &format!("/api/admin/vocabularies/{vid}/terms/{tid}"), None, ) .await; assert_eq!(deleted.status(), StatusCode::NO_CONTENT); let again = send( &app, &cookie, "DELETE", &format!("/api/admin/vocabularies/{vid}/terms/{tid}"), None, ) .await; assert_eq!(again.status(), StatusCode::NOT_FOUND); } #[sqlx::test(migrations = "../db/migrations")] async fn term_edit_delete_requires_auth(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); let app = build_app(state(pool)); let term_uri = "/api/admin/vocabularies/00000000-0000-0000-0000-000000000000/terms/00000000-0000-0000-0000-000000000000"; let patch_resp = app .clone() .oneshot( Request::builder() .method("PATCH") .uri(term_uri) .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"labels":[]}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(patch_resp.status(), StatusCode::UNAUTHORIZED); let delete_resp = app .clone() .oneshot( Request::builder() .method("DELETE") .uri(term_uri) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(delete_resp.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "../db/migrations")] async fn vocabulary_edit_delete_requires_auth(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); let app = build_app(state(pool)); let vocab_uri = "/api/admin/vocabularies/00000000-0000-0000-0000-000000000000"; let patch_resp = app .clone() .oneshot( Request::builder() .method("PATCH") .uri(vocab_uri) .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"key":"x"}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(patch_resp.status(), StatusCode::UNAUTHORIZED); let delete_resp = app .clone() .oneshot( Request::builder() .method("DELETE") .uri(vocab_uri) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(delete_resp.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "../db/migrations")] async fn rename_and_delete_vocabulary(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let v = send( &app, &cookie, "POST", "/api/admin/vocabularies", Some(r#"{"key":"old"}"#), ) .await; let vid: serde_json::Value = serde_json::from_slice(&v.into_body().collect().await.unwrap().to_bytes()).unwrap(); let vid = vid["id"].as_str().unwrap().to_owned(); let renamed = send( &app, &cookie, "PATCH", &format!("/api/admin/vocabularies/{vid}"), Some(r#"{"key":"new"}"#), ) .await; assert_eq!(renamed.status(), StatusCode::NO_CONTENT); let deleted = send( &app, &cookie, "DELETE", &format!("/api/admin/vocabularies/{vid}"), None, ) .await; assert_eq!(deleted.status(), StatusCode::NO_CONTENT); } #[sqlx::test(migrations = "../db/migrations")] async fn delete_vocabulary_with_terms_is_409(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let v = send( &app, &cookie, "POST", "/api/admin/vocabularies", Some(r#"{"key":"material"}"#), ) .await; let vid: serde_json::Value = serde_json::from_slice(&v.into_body().collect().await.unwrap().to_bytes()).unwrap(); let vid = vid["id"].as_str().unwrap().to_owned(); send( &app, &cookie, "POST", &format!("/api/admin/vocabularies/{vid}/terms"), Some(r#"{"external_uri":null,"labels":[{"lang":"sv","label":"Trä"}]}"#), ) .await; let blocked = send( &app, &cookie, "DELETE", &format!("/api/admin/vocabularies/{vid}"), None, ) .await; assert_eq!(blocked.status(), StatusCode::CONFLICT); let body: serde_json::Value = serde_json::from_slice(&blocked.into_body().collect().await.unwrap().to_bytes()).unwrap(); assert_eq!(body["count"], 1); } #[sqlx::test(migrations = "../db/migrations")] async fn delete_authority_referenced_is_409(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; // create an authority let a = send( &app, &cookie, "POST", "/api/admin/authorities", Some(r#"{"kind":"person","external_uri":null,"labels":[{"lang":"sv","label":"Astrid"}]}"#), ) .await; assert_eq!(a.status(), StatusCode::CREATED); let aid: serde_json::Value = serde_json::from_slice(&a.into_body().collect().await.unwrap().to_bytes()).unwrap(); let aid = aid["id"].as_str().unwrap().to_owned(); // create an authority-typed field definition send( &app, &cookie, "POST", "/api/admin/field-definitions", Some( r#"{"key":"maker","data_type":"authority","vocabulary_id":null,"authority_kind":"person","required":false,"group":null,"labels":[{"lang":"sv","label":"Tillverkare"}]}"#, ), ) .await; // create an object let obj = send( &app, &cookie, "POST", "/api/admin/objects", Some( r#"{"object_number":"T-1","object_name":"test object","number_of_objects":1,"visibility":"draft"}"#, ), ) .await; assert_eq!(obj.status(), StatusCode::CREATED); let obj_json: serde_json::Value = serde_json::from_slice(&obj.into_body().collect().await.unwrap().to_bytes()).unwrap(); let obj_id = obj_json["id"].as_str().unwrap().to_owned(); // set the object's maker field to the authority id let fields_body = format!(r#"{{"maker":"{aid}"}}"#); let set = send( &app, &cookie, "PUT", &format!("/api/admin/objects/{obj_id}/fields"), Some(&fields_body), ) .await; assert_eq!(set.status(), StatusCode::NO_CONTENT); // delete the authority — must be blocked let blocked = send( &app, &cookie, "DELETE", &format!("/api/admin/authorities/{aid}"), None, ) .await; assert_eq!(blocked.status(), StatusCode::CONFLICT); let body: serde_json::Value = serde_json::from_slice(&blocked.into_body().collect().await.unwrap().to_bytes()).unwrap(); assert_eq!(body["count"], 1); } #[sqlx::test(migrations = "../db/migrations")] async fn edit_and_delete_authority(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let a = send( &app, &cookie, "POST", "/api/admin/authorities", Some(r#"{"kind":"person","external_uri":null,"labels":[{"lang":"sv","label":"Anon"}]}"#), ) .await; let aid: serde_json::Value = serde_json::from_slice(&a.into_body().collect().await.unwrap().to_bytes()).unwrap(); let aid = aid["id"].as_str().unwrap().to_owned(); let patched = send( &app, &cookie, "PATCH", &format!("/api/admin/authorities/{aid}"), Some(r#"{"external_uri":"https://viaf.org/1","labels":[{"lang":"sv","label":"Astrid"}]}"#), ) .await; assert_eq!(patched.status(), StatusCode::NO_CONTENT); let deleted = send( &app, &cookie, "DELETE", &format!("/api/admin/authorities/{aid}"), None, ) .await; assert_eq!(deleted.status(), StatusCode::NO_CONTENT); } #[sqlx::test(migrations = "../db/migrations")] async fn edit_and_delete_field_definition(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; // create a field definition send( &app, &cookie, "POST", "/api/admin/field-definitions", Some(r#"{"key":"weight","data_type":"integer","vocabulary_id":null,"authority_kind":null,"required":false,"group":null,"labels":[{"lang":"sv","label":"Vikt"}]}"#), ) .await; // PATCH: update required + group + labels let patched = send( &app, &cookie, "PATCH", "/api/admin/field-definitions/weight", Some(r#"{"required":true,"group":"Mått","labels":[{"lang":"sv","label":"Vikt (g)"}]}"#), ) .await; assert_eq!(patched.status(), StatusCode::NO_CONTENT); // PATCH unknown key → 404 let missing = send( &app, &cookie, "PATCH", "/api/admin/field-definitions/nope", Some(r#"{"required":false,"group":null,"labels":[]}"#), ) .await; assert_eq!(missing.status(), StatusCode::NOT_FOUND); // DELETE the (unreferenced) field definition let deleted = send( &app, &cookie, "DELETE", "/api/admin/field-definitions/weight", None, ) .await; assert_eq!(deleted.status(), StatusCode::NO_CONTENT); // DELETE again → 404 let again = send( &app, &cookie, "DELETE", "/api/admin/field-definitions/weight", None, ) .await; assert_eq!(again.status(), StatusCode::NOT_FOUND); } #[sqlx::test(migrations = "../db/migrations")] async fn delete_field_definition_referenced_is_409(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; // create a field definition send( &app, &cookie, "POST", "/api/admin/field-definitions", Some(r#"{"key":"weight","data_type":"integer","vocabulary_id":null,"authority_kind":null,"required":false,"group":null,"labels":[{"lang":"sv","label":"Vikt"}]}"#), ) .await; // create an object and set the field let obj = send( &app, &cookie, "POST", "/api/admin/objects", Some(r#"{"object_number":"T-2","object_name":"test","number_of_objects":1,"visibility":"draft"}"#), ) .await; assert_eq!(obj.status(), StatusCode::CREATED); let obj_json: serde_json::Value = serde_json::from_slice(&obj.into_body().collect().await.unwrap().to_bytes()).unwrap(); let obj_id = obj_json["id"].as_str().unwrap().to_owned(); let set = send( &app, &cookie, "PUT", &format!("/api/admin/objects/{obj_id}/fields"), Some(r#"{"weight":42}"#), ) .await; assert_eq!(set.status(), StatusCode::NO_CONTENT); // delete the field definition — must be blocked let blocked = send( &app, &cookie, "DELETE", "/api/admin/field-definitions/weight", None, ) .await; assert_eq!(blocked.status(), StatusCode::CONFLICT); let body: serde_json::Value = serde_json::from_slice(&blocked.into_body().collect().await.unwrap().to_bytes()).unwrap(); assert_eq!(body["count"], 1); } #[sqlx::test(migrations = "../db/migrations")] async fn listed_object_carries_timestamps(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let created = send( &app, &cookie, "POST", "/api/admin/objects", Some(r#"{"object_number":"TS-1","object_name":"clock","number_of_objects":1,"visibility":"draft"}"#), ) .await; assert_eq!(created.status(), StatusCode::CREATED); let list = send(&app, &cookie, "GET", "/api/admin/objects", None).await; assert_eq!(list.status(), StatusCode::OK); let body: serde_json::Value = serde_json::from_slice(&list.into_body().collect().await.unwrap().to_bytes()).unwrap(); let item = &body["items"][0]; let created_at = item["created_at"].as_str().unwrap(); let updated_at = item["updated_at"].as_str().unwrap(); assert!(!created_at.is_empty(), "created_at must be non-empty"); assert!(!updated_at.is_empty(), "updated_at must be non-empty"); } #[sqlx::test(migrations = "../db/migrations")] async fn list_objects_sort_filter_quick_search(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; let app = build_app(state(pool)); let cookie = login(&app, "ed@example.com", "pw-editor-123").await; let create = |number: &str, name: &str| { format!( r#"{{"object_number":"{number}","object_name":"{name}","number_of_objects":1,"visibility":"draft"}}"# ) }; for (number, name) in [ ("FOO-1", "foo apple"), ("FOO-2", "foo banana"), ("BAR-1", "bar cherry"), ] { let resp = send( &app, &cookie, "POST", "/api/admin/objects", Some(&create(number, name)), ) .await; assert_eq!(resp.status(), StatusCode::CREATED); } // No params → default order is object_number ascending. let default = send(&app, &cookie, "GET", "/api/admin/objects", None).await; let body: serde_json::Value = serde_json::from_slice(&default.into_body().collect().await.unwrap().to_bytes()).unwrap(); let numbers: Vec<&str> = body["items"] .as_array() .unwrap() .iter() .map(|i| i["object_number"].as_str().unwrap()) .collect(); assert_eq!(numbers, ["BAR-1", "FOO-1", "FOO-2"]); assert_eq!(body["total"], 3); // sort=object_name&order=desc&visibility=draft&q=foo let filtered = send( &app, &cookie, "GET", "/api/admin/objects?sort=object_name&order=desc&visibility=draft&q=foo", None, ) .await; assert_eq!(filtered.status(), StatusCode::OK); let body: serde_json::Value = serde_json::from_slice(&filtered.into_body().collect().await.unwrap().to_bytes()).unwrap(); let names: Vec<&str> = body["items"] .as_array() .unwrap() .iter() .map(|i| i["object_name"].as_str().unwrap()) .collect(); // Only the two "foo …" objects, name descending. assert_eq!(names, ["foo banana", "foo apple"]); assert_eq!(body["total"], 2); } #[sqlx::test(migrations = "../db/migrations")] async fn field_definition_edit_delete_requires_auth(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone())) .await .unwrap(); let app = build_app(state(pool)); let patch_resp = app .clone() .oneshot( Request::builder() .method("PATCH") .uri("/api/admin/field-definitions/weight") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"required":false,"group":null,"labels":[]}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(patch_resp.status(), StatusCode::UNAUTHORIZED); let delete_resp = app .clone() .oneshot( Request::builder() .method("DELETE") .uri("/api/admin/field-definitions/weight") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(delete_resp.status(), StatusCode::UNAUTHORIZED); }