test: add integration tests for webauthn, passkeys, and input validation
Add webauthn API tests (register, auth, passkey add flows), passkey endpoint auth tests, bag validation (zero/negative amount, invalid date), brew validation (zero weight, negative grind, zero water, temp > 100), and fix flaky date assertion in bag close test.
This commit is contained in:
parent
09bd7e61e4
commit
590868e51f
6 changed files with 437 additions and 4 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
use reqwest::{Client, StatusCode};
|
use reqwest::{Client, StatusCode};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::helpers::spawn_app_with_auth;
|
use crate::helpers::{spawn_app, spawn_app_with_auth};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_create_token_requires_authentication() {
|
async fn test_create_token_requires_authentication() {
|
||||||
|
|
@ -273,3 +273,66 @@ async fn test_read_endpoints_dont_require_authentication() {
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Admin passkey endpoint tests ---
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_passkeys_requires_auth() {
|
||||||
|
let app = spawn_app().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(&app.api_url("/passkeys"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_passkeys_returns_empty_for_user_without_passkeys() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(&app.api_url("/passkeys"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let passkeys: Vec<serde_json::Value> = response.json().await.expect("Failed to parse response");
|
||||||
|
assert!(passkeys.is_empty(), "expected empty passkeys list");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_passkey_nonexistent_returns_404() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.delete(&app.api_url("/passkeys/99999"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_passkey_requires_auth() {
|
||||||
|
let app = spawn_app().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.delete(&app.api_url("/passkeys/1"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,16 @@ use crate::helpers::{
|
||||||
create_default_bag, create_default_gear, create_default_roast, create_default_roaster,
|
create_default_bag, create_default_gear, create_default_roast, create_default_roaster,
|
||||||
spawn_app, spawn_app_with_auth,
|
spawn_app, spawn_app_with_auth,
|
||||||
};
|
};
|
||||||
|
use crate::test_macros::define_crud_tests;
|
||||||
use brewlog::domain::bags::{Bag, BagWithRoast, NewBag, UpdateBag};
|
use brewlog::domain::bags::{Bag, BagWithRoast, NewBag, UpdateBag};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
define_crud_tests!(
|
||||||
|
entity: bag,
|
||||||
|
path: "/bags",
|
||||||
|
list_type: BagWithRoast
|
||||||
|
);
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn creating_a_bag_returns_a_201_for_valid_data() {
|
async fn creating_a_bag_returns_a_201_for_valid_data() {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -298,9 +305,13 @@ async fn closing_a_bag_automatically_sets_finished_at() {
|
||||||
let updated_bag: Bag = response.json().await.expect("Failed to parse response");
|
let updated_bag: Bag = response.json().await.expect("Failed to parse response");
|
||||||
assert!(updated_bag.closed);
|
assert!(updated_bag.closed);
|
||||||
assert!(updated_bag.finished_at.is_some());
|
assert!(updated_bag.finished_at.is_some());
|
||||||
assert_eq!(
|
// Accept today or yesterday to avoid midnight-boundary flakiness
|
||||||
updated_bag.finished_at.unwrap(),
|
let today = chrono::Utc::now().date_naive();
|
||||||
chrono::Utc::now().date_naive()
|
let yesterday = today - chrono::Duration::days(1);
|
||||||
|
let finished = updated_bag.finished_at.unwrap();
|
||||||
|
assert!(
|
||||||
|
finished == today || finished == yesterday,
|
||||||
|
"expected finished_at to be today or yesterday, got {finished}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,3 +369,67 @@ async fn updating_bag_amount_recomputes_remaining() {
|
||||||
assert_eq!(updated_bag.amount, 500.0);
|
assert_eq!(updated_bag.amount, 500.0);
|
||||||
assert_eq!(updated_bag.remaining, 485.0);
|
assert_eq!(updated_bag.remaining, 485.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_bag_with_zero_amount_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&serde_json::json!({
|
||||||
|
"roast_id": roast.id,
|
||||||
|
"amount": 0.0
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_bag_with_negative_amount_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&serde_json::json!({
|
||||||
|
"roast_id": roast.id,
|
||||||
|
"amount": -100.0
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_bag_with_invalid_date_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/bags"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&serde_json::json!({
|
||||||
|
"roast_id": roast.id,
|
||||||
|
"roast_date": "not-a-date",
|
||||||
|
"amount": 250.0
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -643,3 +643,135 @@ async fn updating_a_nonexistent_brew_returns_404() {
|
||||||
|
|
||||||
assert_eq!(response.status(), 404);
|
assert_eq!(response.status(), 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_brew_with_zero_coffee_weight_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let bag = create_default_bag(&app, roast.id).await;
|
||||||
|
let grinder = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;
|
||||||
|
let brewer = create_default_gear(&app, "brewer", "Hario", "V60 02").await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/brews"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&NewBrew {
|
||||||
|
bag_id: bag.id,
|
||||||
|
coffee_weight: 0.0,
|
||||||
|
grinder_id: grinder.id,
|
||||||
|
grind_setting: 24.0,
|
||||||
|
brewer_id: brewer.id,
|
||||||
|
filter_paper_id: None,
|
||||||
|
water_volume: 250,
|
||||||
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
|
brew_time: None,
|
||||||
|
created_at: None,
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_brew_with_negative_grind_setting_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let bag = create_default_bag(&app, roast.id).await;
|
||||||
|
let grinder = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;
|
||||||
|
let brewer = create_default_gear(&app, "brewer", "Hario", "V60 02").await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/brews"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&NewBrew {
|
||||||
|
bag_id: bag.id,
|
||||||
|
coffee_weight: 15.0,
|
||||||
|
grinder_id: grinder.id,
|
||||||
|
grind_setting: -1.0,
|
||||||
|
brewer_id: brewer.id,
|
||||||
|
filter_paper_id: None,
|
||||||
|
water_volume: 250,
|
||||||
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
|
brew_time: None,
|
||||||
|
created_at: None,
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_brew_with_zero_water_volume_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let bag = create_default_bag(&app, roast.id).await;
|
||||||
|
let grinder = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;
|
||||||
|
let brewer = create_default_gear(&app, "brewer", "Hario", "V60 02").await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/brews"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&NewBrew {
|
||||||
|
bag_id: bag.id,
|
||||||
|
coffee_weight: 15.0,
|
||||||
|
grinder_id: grinder.id,
|
||||||
|
grind_setting: 24.0,
|
||||||
|
brewer_id: brewer.id,
|
||||||
|
filter_paper_id: None,
|
||||||
|
water_volume: 0,
|
||||||
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
|
brew_time: None,
|
||||||
|
created_at: None,
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creating_brew_with_water_temp_over_100_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
let bag = create_default_bag(&app, roast.id).await;
|
||||||
|
let grinder = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;
|
||||||
|
let brewer = create_default_gear(&app, "brewer", "Hario", "V60 02").await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.api_url("/brews"))
|
||||||
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
|
.json(&NewBrew {
|
||||||
|
bag_id: bag.id,
|
||||||
|
coffee_weight: 15.0,
|
||||||
|
grinder_id: grinder.id,
|
||||||
|
grind_setting: 24.0,
|
||||||
|
brewer_id: brewer.id,
|
||||||
|
filter_paper_id: None,
|
||||||
|
water_volume: 250,
|
||||||
|
water_temp: 101.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
|
brew_time: None,
|
||||||
|
created_at: None,
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), 400);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ impl TestApp {
|
||||||
format!("{}/api/v1{}", self.address, path)
|
format!("{}/api/v1{}", self.address, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn webauthn_url(&self, path: &str) -> String {
|
||||||
|
format!("{}/api/v1/webauthn{}", self.address, path)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn page_url(&self, path: &str) -> String {
|
pub fn page_url(&self, path: &str) -> String {
|
||||||
format!("{}{}", self.address, path)
|
format!("{}{}", self.address, path)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,4 @@ pub mod scan_api;
|
||||||
pub mod stats_api;
|
pub mod stats_api;
|
||||||
pub mod test_macros;
|
pub mod test_macros;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
|
pub mod webauthn_api;
|
||||||
|
|
|
||||||
158
tests/server/webauthn_api.rs
Normal file
158
tests/server/webauthn_api.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
use reqwest::{Client, StatusCode};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::helpers::spawn_app_with_auth;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn register_start_with_missing_token_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
// POST without a token field — Axum returns 422 for missing fields
|
||||||
|
let response = client
|
||||||
|
.post(app.webauthn_url("/register/start"))
|
||||||
|
.json(&json!({ "display_name": "Alice" }))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn register_start_with_invalid_token_returns_401_or_gone() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(app.webauthn_url("/register/start"))
|
||||||
|
.json(&json!({
|
||||||
|
"token": "totally-invalid-token",
|
||||||
|
"display_name": "Alice"
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
// Expired/missing token → 401 (token lookup fails) or 410 (token expired)
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
assert!(
|
||||||
|
status == 401 || status == 410,
|
||||||
|
"expected 401 or 410, got {status}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn register_finish_with_invalid_challenge_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
// `credential: {}` is invalid for RegisterPublicKeyCredential, so Axum returns 422
|
||||||
|
let response = client
|
||||||
|
.post(app.webauthn_url("/register/finish"))
|
||||||
|
.json(&json!({
|
||||||
|
"challenge_id": "nonexistent-challenge-id",
|
||||||
|
"passkey_name": "my-key",
|
||||||
|
"credential": {}
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn auth_start_with_no_passkeys_returns_404() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
// No passkeys have been registered, so auth start should fail.
|
||||||
|
// auth/start is a GET endpoint.
|
||||||
|
let response = client
|
||||||
|
.get(app.webauthn_url("/auth/start"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn auth_finish_with_invalid_challenge_returns_400() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
// `credential: {}` is invalid for PublicKeyCredential, so Axum returns 422
|
||||||
|
let response = client
|
||||||
|
.post(app.webauthn_url("/auth/finish"))
|
||||||
|
.json(&json!({
|
||||||
|
"challenge_id": "nonexistent-challenge-id",
|
||||||
|
"credential": {}
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn passkey_add_start_requires_auth() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
// No auth header → 401
|
||||||
|
let response = client
|
||||||
|
.post(app.webauthn_url("/passkey/start"))
|
||||||
|
.json(&json!({ "name": "my-passkey" }))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn passkey_add_finish_requires_auth() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
// No auth header → 401
|
||||||
|
let response = client
|
||||||
|
.post(app.webauthn_url("/passkey/finish"))
|
||||||
|
.json(&json!({
|
||||||
|
"challenge_id": "nonexistent-challenge-id",
|
||||||
|
"name": "my-passkey",
|
||||||
|
"credential": {}
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn auth_start_with_non_localhost_cli_callback_returns_400_or_404() {
|
||||||
|
let app = spawn_app_with_auth().await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
// auth/start is GET. Without passkeys, we get 404 before the callback validation.
|
||||||
|
// With passkeys, a non-localhost callback would return 400.
|
||||||
|
// We verify the endpoint handles these params without crashing.
|
||||||
|
let response = client
|
||||||
|
.get(&format!(
|
||||||
|
"{}/auth/start?cli_callback=http://evil.com/steal&state=abc&token_name=test",
|
||||||
|
app.webauthn_url("")
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
assert!(
|
||||||
|
status == 400 || status == 404,
|
||||||
|
"expected 400 or 404, got {status}"
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue