1use toolkit_canonical_errors::CanonicalError;
11
12use crate::Error;
13use crate::errors::OdataError;
14
15impl From<Error> for CanonicalError {
16 fn from(err: Error) -> Self {
17 use Error::{
18 CursorInvalidBase64, CursorInvalidDirection, CursorInvalidFields, CursorInvalidJson,
19 CursorInvalidKeys, CursorInvalidVersion, Db, FilterMismatch, InvalidCursor,
20 InvalidFilter, InvalidLimit, InvalidOrderByField, OrderMismatch, OrderWithCursor,
21 ParsingUnavailable,
22 };
23
24 match err {
25 InvalidFilter(msg) => OdataError::invalid_argument()
26 .with_field_violation(
27 "$filter",
28 format!("Invalid $filter: {msg}"),
29 "INVALID_FILTER",
30 )
31 .create(),
32
33 InvalidOrderByField(field) => OdataError::invalid_argument()
34 .with_field_violation(
35 "$orderby",
36 format!("Unsupported $orderby field: {field}"),
37 "INVALID_ORDERBY_FIELD",
38 )
39 .create(),
40
41 InvalidCursor => OdataError::invalid_argument()
42 .with_field_violation("cursor", "invalid cursor", "INVALID_CURSOR")
43 .create(),
44
45 CursorInvalidBase64 => OdataError::invalid_argument()
46 .with_field_violation(
47 "cursor",
48 "invalid cursor: invalid base64url encoding",
49 "INVALID_CURSOR",
50 )
51 .create(),
52
53 CursorInvalidJson => OdataError::invalid_argument()
54 .with_field_violation("cursor", "invalid cursor: malformed JSON", "INVALID_CURSOR")
55 .create(),
56
57 CursorInvalidVersion => OdataError::invalid_argument()
58 .with_field_violation(
59 "cursor",
60 "invalid cursor: unsupported version",
61 "INVALID_CURSOR",
62 )
63 .create(),
64
65 CursorInvalidKeys => OdataError::invalid_argument()
66 .with_field_violation(
67 "cursor",
68 "invalid cursor: empty or invalid keys",
69 "INVALID_CURSOR",
70 )
71 .create(),
72
73 CursorInvalidFields => OdataError::invalid_argument()
74 .with_field_violation(
75 "cursor",
76 "invalid cursor: empty or invalid fields",
77 "INVALID_CURSOR",
78 )
79 .create(),
80
81 CursorInvalidDirection => OdataError::invalid_argument()
82 .with_field_violation(
83 "cursor",
84 "invalid cursor: invalid sort direction",
85 "INVALID_CURSOR",
86 )
87 .create(),
88
89 OrderMismatch => OdataError::invalid_argument()
90 .with_field_violation(
91 "cursor",
92 "Order mismatch between cursor and query",
93 "ORDER_MISMATCH",
94 )
95 .create(),
96
97 FilterMismatch => OdataError::invalid_argument()
98 .with_field_violation(
99 "cursor",
100 "Filter mismatch between cursor and query",
101 "FILTER_MISMATCH",
102 )
103 .create(),
104
105 InvalidLimit => OdataError::invalid_argument()
106 .with_field_violation("$top", "Invalid limit parameter", "INVALID_LIMIT")
107 .create(),
108
109 OrderWithCursor => OdataError::invalid_argument()
114 .with_field_violation(
115 "$orderby",
116 "Cannot specify $orderby when cursor is present",
117 "ORDER_WITH_CURSOR",
118 )
119 .with_field_violation(
120 "cursor",
121 "Cannot specify cursor when $orderby is present",
122 "ORDER_WITH_CURSOR",
123 )
124 .create(),
125
126 Db(msg) => CanonicalError::internal(format!("OData Db error: {msg}")).create(),
131
132 ParsingUnavailable(msg) => {
133 CanonicalError::internal(format!("OData parsing unavailable: {msg}")).create()
134 }
135 }
136 }
137}
138
139#[cfg(test)]
140#[cfg_attr(coverage_nightly, coverage(off))]
141mod tests {
142 use super::*;
143 use toolkit_canonical_errors::Problem;
144
145 const ODATA_RESOURCE_TYPE: &str = "gts.cf.core.odata.query.v1~";
146
147 fn wire(err: Error) -> Problem {
148 Problem::from(CanonicalError::from(err))
149 }
150
151 fn field_violations(p: &Problem) -> &Vec<serde_json::Value> {
152 p.context
153 .get("field_violations")
154 .and_then(|v| v.as_array())
155 .expect("invalid_argument context must carry field_violations[]")
156 }
157
158 fn assert_violation(v: &serde_json::Value, field: &str, reason: &str) {
159 assert_eq!(
160 v.get("field").and_then(|x| x.as_str()),
161 Some(field),
162 "unexpected violation field in {v}"
163 );
164 assert_eq!(
165 v.get("reason").and_then(|x| x.as_str()),
166 Some(reason),
167 "unexpected violation reason in {v}"
168 );
169 }
170
171 fn resource_type(p: &Problem) -> &str {
172 p.context
173 .get("resource_type")
174 .and_then(|v| v.as_str())
175 .expect("InvalidArgument from the OData scope must tag resource_type")
176 }
177
178 #[test]
179 fn invalid_filter_populates_field_violation() {
180 let p = wire(Error::InvalidFilter("malformed".into()));
181 assert_eq!(p.status, 400);
182 assert!(p.problem_type.contains("invalid_argument"));
183 assert_eq!(resource_type(&p), ODATA_RESOURCE_TYPE);
184 let violations = field_violations(&p);
185 assert_eq!(violations.len(), 1);
186 assert_violation(&violations[0], "$filter", "INVALID_FILTER");
187 let desc = violations[0]
188 .get("description")
189 .and_then(|x| x.as_str())
190 .unwrap_or_default();
191 assert!(desc.contains("malformed"), "description was {desc:?}");
192 }
193
194 #[test]
195 fn invalid_orderby_field_populates_field_violation() {
196 let p = wire(Error::InvalidOrderByField("unknown".into()));
197 assert_eq!(p.status, 400);
198 assert_eq!(resource_type(&p), ODATA_RESOURCE_TYPE);
199 let violations = field_violations(&p);
200 assert_eq!(violations.len(), 1);
201 assert_violation(&violations[0], "$orderby", "INVALID_ORDERBY_FIELD");
202 }
203
204 #[test]
205 fn cursor_invalid_base64_populates_field_violation() {
206 let p = wire(Error::CursorInvalidBase64);
207 assert_eq!(p.status, 400);
208 assert_eq!(resource_type(&p), ODATA_RESOURCE_TYPE);
209 let violations = field_violations(&p);
210 assert_eq!(violations.len(), 1);
211 assert_violation(&violations[0], "cursor", "INVALID_CURSOR");
212 }
213
214 #[test]
215 fn order_with_cursor_emits_two_violations() {
216 let p = wire(Error::OrderWithCursor);
217 assert_eq!(p.status, 400);
218 assert_eq!(resource_type(&p), ODATA_RESOURCE_TYPE);
219 let violations = field_violations(&p);
220 assert_eq!(
221 violations.len(),
222 2,
223 "OrderWithCursor must surface both `$orderby` and `cursor`"
224 );
225 let fields: Vec<&str> = violations
226 .iter()
227 .filter_map(|v| v.get("field").and_then(|x| x.as_str()))
228 .collect();
229 assert!(fields.contains(&"$orderby"));
230 assert!(fields.contains(&"cursor"));
231 for v in violations {
232 assert_eq!(
233 v.get("reason").and_then(|x| x.as_str()),
234 Some("ORDER_WITH_CURSOR")
235 );
236 }
237 }
238
239 #[test]
240 fn order_mismatch_keys_to_cursor() {
241 let p = wire(Error::OrderMismatch);
242 let violations = field_violations(&p);
243 assert_eq!(violations.len(), 1);
244 assert_violation(&violations[0], "cursor", "ORDER_MISMATCH");
245 }
246
247 #[test]
248 fn filter_mismatch_keys_to_cursor() {
249 let p = wire(Error::FilterMismatch);
250 let violations = field_violations(&p);
251 assert_eq!(violations.len(), 1);
252 assert_violation(&violations[0], "cursor", "FILTER_MISMATCH");
253 }
254
255 #[test]
256 fn invalid_limit_keys_to_top() {
257 let p = wire(Error::InvalidLimit);
258 let violations = field_violations(&p);
259 assert_eq!(violations.len(), 1);
260 assert_violation(&violations[0], "$top", "INVALID_LIMIT");
261 }
262
263 #[test]
264 fn db_error_maps_to_internal_with_diagnostic() {
265 let canonical =
266 CanonicalError::from(Error::Db("connection refused: 127.0.0.1:5432".into()));
267 let p = Problem::from(canonical.clone());
269 assert_eq!(p.status, 500);
270 assert!(p.problem_type.contains("internal"));
271 assert!(!p.detail.contains("127.0.0.1"));
273 let diag = canonical.diagnostic().expect("Internal carries diagnostic");
275 assert!(
276 diag.contains("connection refused"),
277 "diagnostic was {diag:?}"
278 );
279 }
280
281 #[test]
282 fn parsing_unavailable_maps_to_internal_with_diagnostic() {
283 let canonical = CanonicalError::from(Error::ParsingUnavailable("feature off"));
284 assert_eq!(canonical.status_code(), 500);
285 let diag = canonical.diagnostic().expect("Internal carries diagnostic");
286 assert!(diag.contains("feature off"), "diagnostic was {diag:?}");
287 }
288}