//! Every error code of SPEC ยง14.3.2 serialises like `contracts/examples/errors/.json`, with its HTTP status. use std::path::PathBuf; use axum::http::StatusCode; use axum::response::IntoResponse; use http_body_util::BodyExt; use kvitt_server::error::{AppError, ErrorBody, ErrorCode}; use serde_json::Value; const STATUS: &[(&str, u16)] = &[ ("invalid_request", 400), ("invite_invalid", 400), ("unauthorized", 401), ("device_revoked", 401), ("forbidden", 403), ("not_found", 404), ("household_full", 409), ("household_incomplete", 409), ("name_taken", 409), ("account_not_empty", 409), ("category_in_use", 409), ("category_is_default", 409), ("cannot_revoke_current_device", 409), ("no_accounts", 409), ("cursor_ahead", 409), ("payload_too_large", 413), ("unsupported_media_type", 415), ("validation_failed", 422), ("image_invalid", 422), ("batch_too_large", 422), ("protocol_unsupported", 426), ("rate_limited", 429), ("internal", 500), ("not_implemented", 501), ("unavailable", 503), ]; #[test] fn every_code_has_the_status_of_the_spec_table() { assert_eq!(ErrorCode::ALL.len(), STATUS.len()); for (code, (token, status)) in ErrorCode::ALL.iter().zip(STATUS) { assert_eq!(code.as_str(), *token); assert_eq!(code.status().as_u16(), *status, "{token}"); } } #[tokio::test] async fn every_error_example_round_trips_through_app_error() { let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../contracts/examples/errors"); let mut seen = 0; for entry in std::fs::read_dir(&dir).unwrap() { let path = entry.unwrap().path(); let example: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); let body: ErrorBody = serde_json::from_value(example.clone()).unwrap(); let stem = path.file_stem().unwrap().to_string_lossy().into_owned(); assert_eq!( body.code.as_str(), stem, "{}: file name is the code", path.display() ); let mut error = AppError::new(body.code, body.message.clone()); if let Some(field) = &body.field { error = error.with_field(field); } let response = error.into_response(); let status = response.status(); assert_eq!( response.headers()["content-type"], "application/json; charset=utf-8" ); let bytes = response.into_body().collect().await.unwrap().to_bytes(); let json: Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(json, example, "{stem}"); let expected = STATUS .iter() .find(|(t, _)| *t == stem) .map(|(_, s)| *s) .unwrap(); assert_eq!(status, StatusCode::from_u16(expected).unwrap()); seen += 1; } assert_eq!(seen, ErrorCode::ALL.len(), "one example per error code"); } #[test] fn rate_limited_carries_retry_after() { let response = AppError::rate_limited(30).into_response(); assert_eq!(response.headers()["retry-after"], "30"); }