fix(api): drop redundant dev-deps; fix server AppState for cookie_secure; add logout + illegal-transition tests

This commit is contained in:
2026-06-02 15:04:07 +02:00
parent 5135aeee6c
commit 642f709bbe
5 changed files with 101 additions and 2 deletions
-2
View File
@@ -21,5 +21,3 @@ tokio.workspace = true
tower.workspace = true
http-body-util.workspace = true
serde_json.workspace = true
sqlx.workspace = true
auth = { path = "../auth" }
+2
View File
@@ -138,6 +138,8 @@ pub(crate) async fn set_visibility(
Path(id): Path<String>,
Json(req): Json<VisibilityRequest>,
) -> Result<StatusCode, StatusCode> {
// 404 (not 400) for an unparseable id — same non-leaking convention as the public
// surface: never reveal whether an id could exist.
let object_id = id.parse::<ObjectId>().map_err(|_| StatusCode::NOT_FOUND)?;
let mut tx = state
+96
View File
@@ -238,3 +238,99 @@ async fn editor_can_publish_via_admin_endpoint(pool: PgPool) {
let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
assert_eq!(obj.visibility, Visibility::Public);
}
#[sqlx::test(migrations = "../db/migrations")]
async fn logout_invalidates_the_session(pool: PgPool) {
migrate_sessions(&db::Db::from_pool(pool.clone()))
.await
.unwrap();
seed_user(&pool, "admin@example.com", "s3cret-passw0rd", Role::Admin).await;
let app = build_app(state(pool));
let resp = app
.clone()
.oneshot(login_request("admin@example.com", "s3cret-passw0rd"))
.await
.unwrap();
let cookie = session_cookie(&resp);
// logout with the session cookie
let out = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/admin/logout")
.header(header::COOKIE, &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(out.status(), StatusCode::NO_CONTENT);
// the old cookie no longer authenticates
let me = app
.oneshot(
Request::builder()
.uri("/api/admin/me")
.header(header::COOKIE, &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(me.status(), StatusCode::UNAUTHORIZED);
}
#[sqlx::test(migrations = "../db/migrations")]
async fn illegal_visibility_transition_is_409(pool: PgPool) {
migrate_sessions(&db::Db::from_pool(pool.clone()))
.await
.unwrap();
seed_user(&pool, "editor@example.com", "pw-editor-123", Role::Editor).await;
// a draft object — draft -> public in one step is illegal (must pass through internal)
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,
&ObjectInput {
object_number: "D-1".into(),
object_name: "vase".into(),
number_of_objects: 1,
brief_description: None,
current_location: None,
current_owner: None,
recorder: None,
recording_date: None,
visibility: Visibility::Draft,
},
)
.await
.unwrap();
tx.commit().await.unwrap();
let app = build_app(state(pool));
let resp = app
.clone()
.oneshot(login_request("editor@example.com", "pw-editor-123"))
.await
.unwrap();
let cookie = session_cookie(&resp);
let publish = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/api/admin/objects/{id}/visibility"))
.header(header::COOKIE, &cookie)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r#"{"visibility":"public"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(publish.status(), StatusCode::CONFLICT);
}
+2
View File
@@ -20,6 +20,8 @@ pub async fn run(config: Config) -> anyhow::Result<()> {
let state = AppState {
db,
app_name: config.app_name.clone(),
// Wired to config in the auth CLI task; Secure by default.
cookie_secure: true,
};
let listener = TcpListener::bind(&config.bind_addr)
+1
View File
@@ -15,6 +15,7 @@ async fn serves_health_live_over_tcp() {
let state = AppState {
db,
app_name: "Test".to_string(),
cookie_secure: false,
};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();