gatekeep_axum/
test_support.rs1use axum::{
4 body::{Body, to_bytes},
5 http::StatusCode,
6 response::Response,
7};
8use thiserror::Error;
9
10use crate::{DenialBody, DenialError};
11
12const DENIAL_BODY_FIELDS: &[&str] = &["error", "message", "reason"];
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ExpectedDenial {
17 status: StatusCode,
18 error: DenialError,
19 message: Option<String>,
20 reason: ExpectedReason,
21}
22
23impl ExpectedDenial {
24 #[must_use]
26 pub const fn forbidden() -> Self {
27 Self {
28 status: StatusCode::FORBIDDEN,
29 error: DenialError::Forbidden,
30 message: None,
31 reason: ExpectedReason::Any,
32 }
33 }
34
35 #[must_use]
37 pub const fn not_found() -> Self {
38 Self {
39 status: StatusCode::NOT_FOUND,
40 error: DenialError::NotFound,
41 message: None,
42 reason: ExpectedReason::Any,
43 }
44 }
45
46 #[must_use]
48 pub fn with_message(mut self, message: impl Into<String>) -> Self {
49 self.message = Some(message.into());
50 self
51 }
52
53 #[must_use]
55 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
56 self.reason = ExpectedReason::Some(reason.into());
57 self
58 }
59
60 #[must_use]
62 pub fn without_reason(mut self) -> Self {
63 self.reason = ExpectedReason::None;
64 self
65 }
66}
67
68pub async fn assert_denial_response(
78 response: Response<Body>,
79 expected: ExpectedDenial,
80) -> Result<DenialBody, DenialAssertError> {
81 let status = response.status();
82 if status != expected.status {
83 return Err(DenialAssertError::Status {
84 expected: expected.status,
85 actual: status,
86 });
87 }
88
89 let bytes = to_bytes(response.into_body(), usize::MAX)
90 .await
91 .map_err(DenialAssertError::Body)?;
92 let value =
93 serde_json::from_slice::<serde_json::Value>(&bytes).map_err(DenialAssertError::Json)?;
94 let fields = denial_body_fields(&value)?;
95 if fields != DENIAL_BODY_FIELDS {
96 return Err(DenialAssertError::Fields { actual: fields });
97 }
98 let body = serde_json::from_value::<DenialBody>(value).map_err(DenialAssertError::Json)?;
99 if body.error != expected.error {
100 return Err(DenialAssertError::Error {
101 expected: expected.error,
102 actual: body.error,
103 });
104 }
105 if let Some(expected_message) = expected.message
106 && body.message != expected_message
107 {
108 return Err(DenialAssertError::Message {
109 expected: expected_message,
110 actual: body.message,
111 });
112 }
113 match expected.reason {
114 ExpectedReason::None if body.reason.is_some() => {
115 return Err(DenialAssertError::Reason {
116 expected: None,
117 actual: body.reason,
118 });
119 }
120 ExpectedReason::Some(expected_reason)
121 if body.reason.as_deref() != Some(&expected_reason) =>
122 {
123 return Err(DenialAssertError::Reason {
124 expected: Some(expected_reason),
125 actual: body.reason,
126 });
127 }
128 ExpectedReason::Any | ExpectedReason::None | ExpectedReason::Some(_) => {}
129 }
130 Ok(body)
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134enum ExpectedReason {
135 Any,
136 None,
137 Some(String),
138}
139
140fn denial_body_fields(value: &serde_json::Value) -> Result<Vec<String>, DenialAssertError> {
141 let serde_json::Value::Object(object) = value else {
142 return Err(DenialAssertError::Shape);
143 };
144 let mut fields = object.keys().cloned().collect::<Vec<_>>();
145 fields.sort();
146 Ok(fields)
147}
148
149#[derive(Debug, Error)]
151pub enum DenialAssertError {
152 #[error("expected denial status {expected}, got {actual}")]
154 Status {
155 expected: StatusCode,
157 actual: StatusCode,
159 },
160 #[error("failed to read denial body")]
162 Body(#[source] axum::Error),
163 #[error("failed to decode denial body")]
165 Json(#[source] serde_json::Error),
166 #[error("denial body must be a JSON object")]
168 Shape,
169 #[error("expected denial fields [\"error\", \"message\", \"reason\"], got {actual:?}")]
171 Fields {
172 actual: Vec<String>,
174 },
175 #[error("expected denial error {expected:?}, got {actual:?}")]
177 Error {
178 expected: DenialError,
180 actual: DenialError,
182 },
183 #[error("expected denial message {expected:?}, got {actual:?}")]
185 Message {
186 expected: String,
188 actual: String,
190 },
191 #[error("expected denial reason {expected:?}, got {actual:?}")]
193 Reason {
194 expected: Option<String>,
196 actual: Option<String>,
198 },
199}