feat: edit/delete terms — audited, blocked when referenced (#30)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 18:43:02 +02:00
parent f6053068be
commit 09baf2949f
6 changed files with 542 additions and 3 deletions
+126
View File
@@ -333,3 +333,129 @@ async fn creating_a_vocabulary_writes_an_audit_entry(pool: PgPool) {
"expected actor to be a user"
);
}
async fn send(
app: &axum::Router,
cookie: &str,
method: &str,
uri: &str,
body: Option<&str>,
) -> axum::http::Response<Body> {
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);
}