• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

polyphony-chat / sonata / 16378096239

18 Jul 2025 06:49PM UTC coverage: 91.622% (+17.6%) from 74.035%
16378096239

push

github

bitfl0wer
chore: fix tests, add coverage

17 of 52 branches covered (32.69%)

Branch coverage included in aggregate %.

105 of 105 new or added lines in 1 file covered. (100.0%)

5 existing lines in 3 files now uncovered.

661 of 688 relevant lines covered (96.08%)

3760.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.38
/src/errors.rs
1
// This Source Code Form is subject to the terms of the Mozilla Public
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4

5
use poem::{IntoResponse, Response, error::ResponseError, http::StatusCode};
6
use serde::{Deserialize, Serialize};
7
use serde_json::json;
8
use serde_with::{DeserializeFromStr, SerializeDisplay};
9

10
/// Generic error type.
11
pub(crate) type StdError = Box<dyn std::error::Error + Sync + Send + 'static>;
12
/// Generic result type.
13
pub(crate) type StdResult<T> = Result<T, StdError>;
14

15
#[derive(Debug, Serialize, Deserialize)]
16
#[serde(rename_all = "camelCase")]
17
/// A polyproto core Error, with an [Errcode], an error message and optional
18
/// error [Context].
19
///
20
/// Convenience struct to make poem-compatible and unified error returning
21
/// easier
22
pub struct Error {
23
        /// The error code [Errcode], giving a rough idea of what went wrong
24
        pub code: Errcode,
25
        /// An error message, providing some further information about the category
26
        /// of error encountered.
27
        pub message: String,
28
        #[serde(skip_serializing_if = "Option::is_none")]
29
        #[serde(default)]
30
        /// Optional error context.
31
        ///
32
        /// ## Example
33
        ///
34
        /// If a password has to be at least 8
35
        /// characters long, but the user only supplied 6, the context field could
36
        /// tell the user that the field `password` in their request is wrong, and
37
        /// supply a very fine-grained error message, telling the user that they
38
        /// only supplied 6 characters, while 8 were required.
39
        pub context: Option<Context>,
40
}
41

42
impl IntoResponse for Error {
43
        #[cfg_attr(coverage_nightly, coverage(off))]
44
        fn into_response(self) -> Response {
45
                Response::builder()
46
                        .content_type("application/json")
47
                        .status(self.code.status())
48
                        .body(self.to_json())
49
        }
50
}
51

52
impl ResponseError for Error {
53
        #[cfg_attr(coverage_nightly, coverage(off))]
54
        fn status(&self) -> StatusCode {
55
                self.code.status()
56
        }
57
}
58

59
impl From<sqlx::Error> for Error {
60
        #[cfg_attr(coverage_nightly, coverage(off))]
61
        fn from(value: sqlx::Error) -> Self {
62
                log::error!("{value}");
63
                Error::new(Errcode::Internal, None)
64
        }
65
}
66

67
impl From<Error> for poem::Error {
68
        #[cfg_attr(coverage_nightly, coverage(off))]
69
        fn from(value: Error) -> Self {
70
                poem::Error::from_response(value.into_response())
71
        }
72
}
73

74
/// Error message for a wrong username or password.
75
pub const ERROR_WRONG_LOGIN: &str = "The provided login name or password was incorrect.";
76

77
impl Error {
78
        /// Performs the conversion of a shared reference to [Self] into JSON,
79
        /// formatted as a string.
80
        #[must_use]
81
        pub fn to_json(&self) -> String {
3✔
82
                json!(self).to_string()
3✔
83
        }
3✔
84

85
        /// Creates [Self].
86
        #[must_use]
87
        pub fn new(code: Errcode, context: Option<Context>) -> Self {
16✔
88
                Self { code, message: code.message(), context }
16✔
89
        }
16✔
90

91
        /// Creates a variant of [Self] which indicates to a client, that the
92
        /// provided combination of login name and password was incorrect, without
93
        /// telling the client what the concrete issue was.
94
        ///
95
        /// This helper method is useful, because inconsistencies between error
96
        /// messages indicating wrong login credentials could potentially leave an
97
        /// attacker with information about internal state they are not supposed to
98
        /// know about.
99
        #[must_use = "Not returning this variant as a response opens up the possibility of leaking internal state!"]
100
        pub fn new_invalid_login() -> Self {
1✔
101
                Error::new(
1✔
102
                        Errcode::Unauthorized,
1✔
103
                        Some(Context::new(None, None, None, Some(ERROR_WRONG_LOGIN))),
1✔
104
                )
105
        }
1✔
106

107
        /// Creates a variant of [Self] with an [Errcode] of `Errcode::Internal` and
108
        /// an optional, given message.
109
        pub fn new_internal_error(message: Option<&str>) -> Self {
1✔
110
                Self::new(Errcode::Internal, Some(Context::new(None, None, None, message)))
1✔
111
        }
1✔
112

113
        /// Creates a variant of [Self] with an [Errcode] of `Errcode::Duplicate`
114
        /// and an optional, given message.
115
        pub fn new_duplicate_error(message: Option<&str>) -> Self {
1✔
116
                Self::new(Errcode::Duplicate, Some(Context::new(None, None, None, message)))
1✔
117
        }
1✔
118
}
119

120
#[derive(
121
        Debug,
122
        Clone,
123
        Copy,
124
        PartialEq,
UNCOV
125
        DeserializeFromStr,
×
126
        SerializeDisplay,
127
        strum::Display,
128
        strum::EnumString,
129
)]
130
/// Standardized polyproto core error codes, giving a rough idea of what went
131
/// wrong.
132
pub enum Errcode {
133
        #[strum(serialize = "P2_CORE_INTERNAL")]
134
        /// An internal error occurred.
135
        Internal,
136
        #[strum(serialize = "P2_CORE_UNAUTHORIZED")]
137
        /// Unauthorized
138
        Unauthorized,
139
        #[strum(serialize = "P2_CORE_DUPLICATE")]
140
        /// The resource already exists, and the context does not allow for
141
        /// duplicate resources
142
        Duplicate,
143
        #[strum(serialize = "P2_CORE_ILLEGAL_INPUT")]
144
        /// One or many parts of the given input did not succeed validation against
145
        /// context-specific criteria
146
        IllegalInput,
147
}
148

149
impl Errcode {
150
        /// Get an error message, describing what the error code itself means.
151
        pub fn message(&self) -> String {
20✔
152
                match self {
20✔
153
    Errcode::Internal => {
154
                                "An internal error has occurred and this request cannot be processed further"
4✔
155
                                        .to_owned()
4✔
156
                        }
157
    Errcode::Unauthorized => {
158
                                "This action requires authorization, proof of which was not granted".to_owned()
3✔
159
                        }
160
    Errcode::Duplicate => {
161
                                "Creation of the resource is not possible, as it already exists".to_owned()
5✔
162
                        }
163
    Errcode::IllegalInput => "The overall input is well-formed, but one or more of the input fields fail validation criteria".to_owned(),
8✔
164
            }
165
        }
20✔
166
}
167

168
impl ResponseError for Errcode {
169
        #[cfg_attr(coverage_nightly, coverage(off))]
170
        fn status(&self) -> StatusCode {
171
                match self {
172
                        Errcode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
173
                        Errcode::Unauthorized => StatusCode::UNAUTHORIZED,
174
                        Errcode::Duplicate => StatusCode::CONFLICT,
175
                        Errcode::IllegalInput => StatusCode::BAD_REQUEST,
176
                }
177
        }
178
}
179

180
#[derive(Debug, Serialize, Deserialize)]
181
#[serde(rename_all = "camelCase")]
182
/// Optional error context.
183
///
184
/// ## Example
185
///
186
/// If a password has to be at least 8
187
/// characters long, but the user only supplied 6, the context field could
188
/// tell the user that the `field_name` "`password`" in their request is wrong,
189
/// and supply a very fine-grained error message, telling the user that they
190
/// only supplied 6 characters, while 8 were required.
191
pub struct Context {
192
        #[serde(skip_serializing_if = "String::is_empty")]
193
        #[serde(default)]
194
        /// The name of the request body field which caused the error
195
        pub field_name: String,
196
        #[serde(skip_serializing_if = "String::is_empty")]
197
        #[serde(default)]
198
        /// The value that was found to be fault inside the `field_name`
199
        pub found: String,
200
        #[serde(skip_serializing_if = "String::is_empty")]
201
        #[serde(default)]
202
        /// The value that was expected
203
        pub expected: String,
204
        #[serde(skip_serializing_if = "String::is_empty")]
205
        #[serde(default)]
206
        /// An optional, additional, human-readable error message
207
        pub message: String,
208
}
209

210
impl Context {
211
        /// Creates [Self].
212
        pub fn new(
14✔
213
                field_name: Option<&str>,
14✔
214
                found: Option<&str>,
14✔
215
                expected: Option<&str>,
14✔
216
                message: Option<&str>,
14✔
217
        ) -> Self {
14✔
218
                Self {
14✔
219
                        field_name: field_name.map(String::from).unwrap_or_default(),
14✔
220
                        found: found.map(String::from).unwrap_or_default(),
14✔
221
                        expected: expected.map(String::from).unwrap_or_default(),
14✔
222
                        message: message.map(String::from).unwrap_or_default(),
14✔
223
                }
14✔
224
        }
14✔
225
}
226

227
#[cfg(test)]
228
mod tests {
229
        use super::*;
230

231
        #[test]
232
        fn test_error_serialization() {
1✔
233
                let context = Context::new(Some("field"), Some("value"), Some("expected"), Some("message"));
1✔
234
                let error = Error::new(Errcode::IllegalInput, Some(context));
1✔
235

236
                let serialized = serde_json::to_string(&error).unwrap();
1✔
237
                println!("{serialized}");
1✔
238
                let deserialized: Error = serde_json::from_str(&serialized).unwrap();
1✔
239
                println!("{deserialized:#?}");
1✔
240

241
                assert_eq!(deserialized.code, error.code);
1✔
242
                assert_eq!(deserialized.message, error.message);
1✔
243
                assert!(deserialized.context.is_some());
1✔
244
                let ctx = deserialized.context.unwrap();
1✔
245
                assert_eq!(ctx.field_name, "field");
1✔
246
                assert_eq!(ctx.found, "value");
1✔
247
                assert_eq!(ctx.expected, "expected");
1✔
248
                assert_eq!(ctx.message, "message");
1✔
249
        }
1✔
250

251
        #[test]
252
        fn test_error_without_context() {
1✔
253
                let error = Error::new(Errcode::Internal, None);
1✔
254

255
                assert_eq!(error.code, Errcode::Internal);
1✔
256
                assert_eq!(
1✔
257
                        error.message,
258
                        "An internal error has occurred and this request cannot be processed further"
259
                );
260
                assert!(error.context.is_none());
1✔
261
        }
1✔
262

263
        #[test]
264
        fn test_error_to_json() {
1✔
265
                let context = Context::new(Some("username"), Some("admin"), Some("valid username"), None);
1✔
266
                let error = Error::new(Errcode::Duplicate, Some(context));
1✔
267

268
                let json = error.to_json();
1✔
269
                assert!(json.contains("P2_CORE_DUPLICATE"));
1✔
270
                assert!(json.contains("username"));
1✔
271
                assert!(json.contains("admin"));
1✔
272
                assert!(json.contains("valid username"));
1✔
273
        }
1✔
274

275
        #[test]
276
        fn test_error_new_invalid_login() {
1✔
277
                let error = Error::new_invalid_login();
1✔
278

279
                assert_eq!(error.code, Errcode::Unauthorized);
1✔
280
                assert_eq!(
1✔
281
                        error.message,
282
                        "This action requires authorization, proof of which was not granted"
283
                );
284
                assert!(error.context.is_some());
1✔
285
                let ctx = error.context.unwrap();
1✔
286
                assert_eq!(ctx.message, ERROR_WRONG_LOGIN);
1✔
287
        }
1✔
288

289
        #[test]
290
        fn test_error_new_internal_error() {
1✔
291
                let error = Error::new_internal_error(Some("Database connection failed"));
1✔
292

293
                assert_eq!(error.code, Errcode::Internal);
1✔
294
                assert!(error.context.is_some());
1✔
295
                let ctx = error.context.unwrap();
1✔
296
                assert_eq!(ctx.message, "Database connection failed");
1✔
297
        }
1✔
298

299
        #[test]
300
        fn test_error_new_duplicate_error() {
1✔
301
                let error = Error::new_duplicate_error(Some("User already exists"));
1✔
302

303
                assert_eq!(error.code, Errcode::Duplicate);
1✔
304
                assert!(error.context.is_some());
1✔
305
                let ctx = error.context.unwrap();
1✔
306
                assert_eq!(ctx.message, "User already exists");
1✔
307
        }
1✔
308

309
        #[test]
310
        fn test_errcode_messages() {
1✔
311
                assert_eq!(
1✔
312
                        Errcode::Internal.message(),
1✔
313
                        "An internal error has occurred and this request cannot be processed further"
314
                );
315
                assert_eq!(
1✔
316
                        Errcode::Unauthorized.message(),
1✔
317
                        "This action requires authorization, proof of which was not granted"
318
                );
319
                assert_eq!(
1✔
320
                        Errcode::Duplicate.message(),
1✔
321
                        "Creation of the resource is not possible, as it already exists"
322
                );
323
                assert_eq!(
1✔
324
                        Errcode::IllegalInput.message(),
1✔
325
                        "The overall input is well-formed, but one or more of the input fields fail validation criteria"
326
                );
327
        }
1✔
328

329
        #[test]
330
        fn test_errcode_status_codes() {
1✔
331
                use poem::http::StatusCode;
332

333
                assert_eq!(Errcode::Internal.status(), StatusCode::INTERNAL_SERVER_ERROR);
1✔
334
                assert_eq!(Errcode::Unauthorized.status(), StatusCode::UNAUTHORIZED);
1✔
335
                assert_eq!(Errcode::Duplicate.status(), StatusCode::CONFLICT);
1✔
336
                assert_eq!(Errcode::IllegalInput.status(), StatusCode::BAD_REQUEST);
1✔
337
        }
1✔
338

339
        #[test]
340
        fn test_errcode_serialization() {
1✔
341
                let internal = Errcode::Internal;
1✔
342
                let serialized = serde_json::to_string(&internal).unwrap();
1✔
343
                assert_eq!(serialized, "\"P2_CORE_INTERNAL\"");
1✔
344

345
                let deserialized: Errcode = serde_json::from_str(&serialized).unwrap();
1✔
346
                assert_eq!(deserialized, Errcode::Internal);
1✔
347
        }
1✔
348

349
        #[test]
350
        fn test_context_new() {
1✔
351
                let context =
1✔
352
                        Context::new(Some("password"), Some("weak"), Some("strong"), Some("Password too weak"));
1✔
353

354
                assert_eq!(context.field_name, "password");
1✔
355
                assert_eq!(context.found, "weak");
1✔
356
                assert_eq!(context.expected, "strong");
1✔
357
                assert_eq!(context.message, "Password too weak");
1✔
358
        }
1✔
359

360
        #[test]
361
        fn test_context_new_with_none_values() {
1✔
362
                let context = Context::new(None, None, None, Some("General error"));
1✔
363

364
                assert!(context.field_name.is_empty());
1✔
365
                assert!(context.found.is_empty());
1✔
366
                assert!(context.expected.is_empty());
1✔
367
                assert_eq!(context.message, "General error");
1✔
368
        }
1✔
369

370
        #[test]
371
        fn test_error_into_response() {
1✔
372
                let error = Error::new(Errcode::IllegalInput, None);
1✔
373
                let response = error.into_response();
1✔
374

375
                assert_eq!(response.status(), poem::http::StatusCode::BAD_REQUEST);
1✔
376
                assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
1✔
377
        }
1✔
378

379
        #[test]
380
        fn test_error_from_sqlx_error() {
1✔
381
                use sqlx::Error as SqlxError;
382

383
                let sqlx_error = SqlxError::RowNotFound;
1✔
384
                let error: Error = sqlx_error.into();
1✔
385

386
                assert_eq!(error.code, Errcode::Internal);
1✔
387
                assert!(error.context.is_none());
1✔
388
        }
1✔
389

390
        #[test]
391
        fn test_error_into_poem_error() {
1✔
392
                let error = Error::new(Errcode::Unauthorized, None);
1✔
393
                let poem_error: poem::Error = error.into();
1✔
394

395
                // We can't directly test the poem::Error contents, but we can ensure the
396
                // conversion works
397
                assert_eq!(poem_error.status(), poem::http::StatusCode::UNAUTHORIZED);
1✔
398
        }
1✔
399

400
        #[test]
401
        fn test_errcode_display() {
1✔
402
                assert_eq!(Errcode::Internal.to_string(), "P2_CORE_INTERNAL");
1✔
403
                assert_eq!(Errcode::Unauthorized.to_string(), "P2_CORE_UNAUTHORIZED");
1✔
404
                assert_eq!(Errcode::Duplicate.to_string(), "P2_CORE_DUPLICATE");
1✔
405
                assert_eq!(Errcode::IllegalInput.to_string(), "P2_CORE_ILLEGAL_INPUT");
1✔
406
        }
1✔
407

408
        #[test]
409
        fn test_errcode_from_str() {
1✔
410
                use std::str::FromStr;
411

412
                assert_eq!(Errcode::from_str("P2_CORE_INTERNAL").unwrap(), Errcode::Internal);
1✔
413
                assert_eq!(Errcode::from_str("P2_CORE_UNAUTHORIZED").unwrap(), Errcode::Unauthorized);
1✔
414
                assert_eq!(Errcode::from_str("P2_CORE_DUPLICATE").unwrap(), Errcode::Duplicate);
1✔
415
                assert_eq!(Errcode::from_str("P2_CORE_ILLEGAL_INPUT").unwrap(), Errcode::IllegalInput);
1✔
416

417
                assert!(Errcode::from_str("INVALID_CODE").is_err());
1✔
418
        }
1✔
419
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc