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

polyphony-chat / sonata / 16393346128

19 Jul 2025 10:26PM UTC coverage: 86.57% (-4.5%) from 91.038%
16393346128

push

github

bitfl0wer
chore: add tests, fix tests

43 of 92 branches covered (46.74%)

Branch coverage included in aggregate %.

795 of 876 relevant lines covered (90.75%)

2954.79 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
/// Error message to log when converting an [AlgorithmIdentifierOwner] to DER
16
/// encoding fails.
17
pub(crate) const ALGORITHM_IDENTIFER_TO_DER_ERROR_MESSAGE: &str =
18
    "Error encoding signature algorithm parameters to DER:";
19
/// Error message to log when an insertion into the database fails, beacuse user
20
/// data contained unsupported cryptographic primitives.
21
///
22
/// ## Formatting
23
///
24
/// in `format!()`, this variable should be used as follows:
25
///
26
/// ```
27
/// use crate::errors::CONTAINS_UNKNOWN_CRYPTO_ALGOS_ERROR_MESSAGE;
28
///
29
/// fn function() {
30
///     format!("ID-Cert {CONTAINS_UNKNOWN_CRYPTO_ALGOS_ERROR_MESSAGE}");
31
///     format!("Public Key {CONTAINS_UNKNOWN_CRYPTO_ALGOS_ERROR_MESSAGE}");
32
/// }
33
/// ```
34
pub(crate) const CONTAINS_UNKNOWN_CRYPTO_ALGOS_ERROR_MESSAGE: &str =
35
    "contains cryptographic algorithms not supported by this server";
36

37
#[derive(Debug, Serialize, Deserialize)]
38
#[serde(rename_all = "camelCase")]
39
/// A polyproto core Error, with an [Errcode], an error message and optional
40
/// error [Context].
41
///
42
/// Convenience struct to make poem-compatible and unified error returning
43
/// easier
44
pub struct Error {
45
    /// The error code [Errcode], giving a rough idea of what went wrong
46
    pub code: Errcode,
47
    /// An error message, providing some further information about the category
48
    /// of error encountered.
49
    pub message: String,
50
    #[serde(skip_serializing_if = "Option::is_none")]
51
    #[serde(default)]
52
    /// Optional error context.
53
    ///
54
    /// ## Example
55
    ///
56
    /// If a password has to be at least 8
57
    /// characters long, but the user only supplied 6, the context field could
58
    /// tell the user that the field `password` in their request is wrong, and
59
    /// supply a very fine-grained error message, telling the user that they
60
    /// only supplied 6 characters, while 8 were required.
61
    pub context: Option<Context>,
62
}
63

64
impl IntoResponse for Error {
65
    #[cfg_attr(coverage_nightly, coverage(off))]
66
    fn into_response(self) -> Response {
67
        Response::builder()
68
            .content_type("application/json")
69
            .status(self.code.status())
70
            .body(self.to_json())
71
    }
72
}
73

74
impl ResponseError for Error {
75
    #[cfg_attr(coverage_nightly, coverage(off))]
76
    fn status(&self) -> StatusCode {
77
        self.code.status()
78
    }
79
}
80

81
impl From<sqlx::Error> for Error {
82
    #[cfg_attr(coverage_nightly, coverage(off))]
83
    fn from(value: sqlx::Error) -> Self {
84
        log::error!("{value}");
85
        Error::new(Errcode::Internal, None)
86
    }
87
}
88

89
impl From<Error> for poem::Error {
90
    #[cfg_attr(coverage_nightly, coverage(off))]
91
    fn from(value: Error) -> Self {
92
        poem::Error::from_response(value.into_response())
93
    }
94
}
95

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

99
impl Error {
100
    /// Performs the conversion of a shared reference to [Self] into JSON,
101
    /// formatted as a string.
102
    #[must_use]
103
    pub fn to_json(&self) -> String {
3✔
104
        json!(self).to_string()
3✔
105
    }
3✔
106

107
    /// Creates [Self].
108
    #[must_use]
109
    pub fn new(code: Errcode, context: Option<Context>) -> Self {
22✔
110
        Self { code, message: code.message(), context }
22✔
111
    }
22✔
112

113
    /// Creates a variant of [Self] which indicates to a client, that the
114
    /// provided combination of login name and password was incorrect, without
115
    /// telling the client what the concrete issue was.
116
    ///
117
    /// This helper method is useful, because inconsistencies between error
118
    /// messages indicating wrong login credentials could potentially leave an
119
    /// attacker with information about internal state they are not supposed to
120
    /// know about.
121
    #[must_use = "Not returning this variant as a response opens up the possibility of leaking internal state!"]
122
    pub fn new_invalid_login() -> Self {
1✔
123
        Error::new(
1✔
124
            Errcode::Unauthorized,
1✔
125
            Some(Context::new(None, None, None, Some(ERROR_WRONG_LOGIN))),
1✔
126
        )
127
    }
1✔
128

129
    /// Creates a variant of [Self] with an [Errcode] of `Errcode::Internal` and
130
    /// an optional, given message.
131
    pub fn new_internal_error(message: Option<&str>) -> Self {
5✔
132
        Self::new(Errcode::Internal, Some(Context::new(None, None, None, message)))
5✔
133
    }
5✔
134

135
    /// Creates a variant of [Self] with an [Errcode] of `Errcode::Duplicate`
136
    /// and an optional, given message.
137
    pub fn new_duplicate_error(message: Option<&str>) -> Self {
1✔
138
        Self::new(Errcode::Duplicate, Some(Context::new(None, None, None, message)))
1✔
139
    }
1✔
140
}
141

142
#[derive(
143
    Debug,
144
    Clone,
145
    Copy,
146
    PartialEq,
147
    DeserializeFromStr,
×
148
    SerializeDisplay,
149
    strum::Display,
150
    strum::EnumString,
151
)]
152
/// Standardized polyproto core error codes, giving a rough idea of what went
153
/// wrong.
154
pub enum Errcode {
155
    #[strum(serialize = "P2_CORE_INTERNAL")]
156
    /// An internal error occurred.
157
    Internal,
158
    #[strum(serialize = "P2_CORE_UNAUTHORIZED")]
159
    /// Unauthorized
160
    Unauthorized,
161
    #[strum(serialize = "P2_CORE_DUPLICATE")]
162
    /// The resource already exists, and the context does not allow for
163
    /// duplicate resources
164
    Duplicate,
165
    #[strum(serialize = "P2_CORE_ILLEGAL_INPUT")]
166
    /// One or many parts of the given input did not succeed validation against
167
    /// context-specific criteria
168
    IllegalInput,
169
}
170

171
impl Errcode {
172
    /// Get an error message, describing what the error code itself means.
173
    pub fn message(&self) -> String {
26✔
174
        match self {
26✔
175
    Errcode::Internal => {
176
                                "An internal error has occurred and this request cannot be processed further"
10✔
177
                                        .to_owned()
10✔
178
                        }
179
    Errcode::Unauthorized => {
180
                                "This action requires authorization, proof of which was not granted".to_owned()
3✔
181
                        }
182
    Errcode::Duplicate => {
183
                                "Creation of the resource is not possible, as it already exists".to_owned()
5✔
184
                        }
185
    Errcode::IllegalInput => "The overall input is well-formed, but one or more of the input fields fail validation criteria".to_owned(),
8✔
186
            }
187
    }
26✔
188
}
189

190
impl ResponseError for Errcode {
191
    #[cfg_attr(coverage_nightly, coverage(off))]
192
    fn status(&self) -> StatusCode {
193
        match self {
194
            Errcode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
195
            Errcode::Unauthorized => StatusCode::UNAUTHORIZED,
196
            Errcode::Duplicate => StatusCode::CONFLICT,
197
            Errcode::IllegalInput => StatusCode::BAD_REQUEST,
198
        }
199
    }
200
}
201

202
#[derive(Debug, Serialize, Deserialize)]
203
#[serde(rename_all = "camelCase")]
204
/// Optional error context.
205
///
206
/// ## Example
207
///
208
/// If a password has to be at least 8
209
/// characters long, but the user only supplied 6, the context field could
210
/// tell the user that the `field_name` "`password`" in their request is wrong,
211
/// and supply a very fine-grained error message, telling the user that they
212
/// only supplied 6 characters, while 8 were required.
213
pub struct Context {
214
    #[serde(skip_serializing_if = "String::is_empty")]
215
    #[serde(default)]
216
    /// The name of the request body field which caused the error
217
    pub field_name: String,
218
    #[serde(skip_serializing_if = "String::is_empty")]
219
    #[serde(default)]
220
    /// The value that was found to be fault inside the `field_name`
221
    pub found: String,
222
    #[serde(skip_serializing_if = "String::is_empty")]
223
    #[serde(default)]
224
    /// The value that was expected
225
    pub expected: String,
226
    #[serde(skip_serializing_if = "String::is_empty")]
227
    #[serde(default)]
228
    /// An optional, additional, human-readable error message
229
    pub message: String,
230
}
231

232
impl Context {
233
    /// Creates [Self].
234
    pub fn new(
18✔
235
        field_name: Option<&str>,
18✔
236
        found: Option<&str>,
18✔
237
        expected: Option<&str>,
18✔
238
        message: Option<&str>,
18✔
239
    ) -> Self {
18✔
240
        Self {
18✔
241
            field_name: field_name.map(String::from).unwrap_or_default(),
18✔
242
            found: found.map(String::from).unwrap_or_default(),
18✔
243
            expected: expected.map(String::from).unwrap_or_default(),
18✔
244
            message: message.map(String::from).unwrap_or_default(),
18✔
245
        }
18✔
246
    }
18✔
247
}
248

249
#[cfg(test)]
250
mod tests {
251
    use super::*;
252

253
    #[test]
254
    fn test_error_serialization() {
1✔
255
        let context = Context::new(Some("field"), Some("value"), Some("expected"), Some("message"));
1✔
256
        let error = Error::new(Errcode::IllegalInput, Some(context));
1✔
257

258
        let serialized = serde_json::to_string(&error).unwrap();
1✔
259
        println!("{serialized}");
1✔
260
        let deserialized: Error = serde_json::from_str(&serialized).unwrap();
1✔
261
        println!("{deserialized:#?}");
1✔
262

263
        assert_eq!(deserialized.code, error.code);
1✔
264
        assert_eq!(deserialized.message, error.message);
1✔
265
        assert!(deserialized.context.is_some());
1✔
266
        let ctx = deserialized.context.unwrap();
1✔
267
        assert_eq!(ctx.field_name, "field");
1✔
268
        assert_eq!(ctx.found, "value");
1✔
269
        assert_eq!(ctx.expected, "expected");
1✔
270
        assert_eq!(ctx.message, "message");
1✔
271
    }
1✔
272

273
    #[test]
274
    fn test_error_without_context() {
1✔
275
        let error = Error::new(Errcode::Internal, None);
1✔
276

277
        assert_eq!(error.code, Errcode::Internal);
1✔
278
        assert_eq!(
1✔
279
            error.message,
280
            "An internal error has occurred and this request cannot be processed further"
281
        );
282
        assert!(error.context.is_none());
1✔
283
    }
1✔
284

285
    #[test]
286
    fn test_error_to_json() {
1✔
287
        let context = Context::new(Some("username"), Some("admin"), Some("valid username"), None);
1✔
288
        let error = Error::new(Errcode::Duplicate, Some(context));
1✔
289

290
        let json = error.to_json();
1✔
291
        assert!(json.contains("P2_CORE_DUPLICATE"));
1✔
292
        assert!(json.contains("username"));
1✔
293
        assert!(json.contains("admin"));
1✔
294
        assert!(json.contains("valid username"));
1✔
295
    }
1✔
296

297
    #[test]
298
    fn test_error_new_invalid_login() {
1✔
299
        let error = Error::new_invalid_login();
1✔
300

301
        assert_eq!(error.code, Errcode::Unauthorized);
1✔
302
        assert_eq!(
1✔
303
            error.message,
304
            "This action requires authorization, proof of which was not granted"
305
        );
306
        assert!(error.context.is_some());
1✔
307
        let ctx = error.context.unwrap();
1✔
308
        assert_eq!(ctx.message, ERROR_WRONG_LOGIN);
1✔
309
    }
1✔
310

311
    #[test]
312
    fn test_error_new_internal_error() {
1✔
313
        let error = Error::new_internal_error(Some("Database connection failed"));
1✔
314

315
        assert_eq!(error.code, Errcode::Internal);
1✔
316
        assert!(error.context.is_some());
1✔
317
        let ctx = error.context.unwrap();
1✔
318
        assert_eq!(ctx.message, "Database connection failed");
1✔
319
    }
1✔
320

321
    #[test]
322
    fn test_error_new_duplicate_error() {
1✔
323
        let error = Error::new_duplicate_error(Some("User already exists"));
1✔
324

325
        assert_eq!(error.code, Errcode::Duplicate);
1✔
326
        assert!(error.context.is_some());
1✔
327
        let ctx = error.context.unwrap();
1✔
328
        assert_eq!(ctx.message, "User already exists");
1✔
329
    }
1✔
330

331
    #[test]
332
    fn test_errcode_messages() {
1✔
333
        assert_eq!(
1✔
334
            Errcode::Internal.message(),
1✔
335
            "An internal error has occurred and this request cannot be processed further"
336
        );
337
        assert_eq!(
1✔
338
            Errcode::Unauthorized.message(),
1✔
339
            "This action requires authorization, proof of which was not granted"
340
        );
341
        assert_eq!(
1✔
342
            Errcode::Duplicate.message(),
1✔
343
            "Creation of the resource is not possible, as it already exists"
344
        );
345
        assert_eq!(
1✔
346
            Errcode::IllegalInput.message(),
1✔
347
            "The overall input is well-formed, but one or more of the input fields fail validation criteria"
348
        );
349
    }
1✔
350

351
    #[test]
352
    fn test_errcode_status_codes() {
1✔
353
        use poem::http::StatusCode;
354

355
        assert_eq!(Errcode::Internal.status(), StatusCode::INTERNAL_SERVER_ERROR);
1✔
356
        assert_eq!(Errcode::Unauthorized.status(), StatusCode::UNAUTHORIZED);
1✔
357
        assert_eq!(Errcode::Duplicate.status(), StatusCode::CONFLICT);
1✔
358
        assert_eq!(Errcode::IllegalInput.status(), StatusCode::BAD_REQUEST);
1✔
359
    }
1✔
360

361
    #[test]
362
    fn test_errcode_serialization() {
1✔
363
        let internal = Errcode::Internal;
1✔
364
        let serialized = serde_json::to_string(&internal).unwrap();
1✔
365
        assert_eq!(serialized, "\"P2_CORE_INTERNAL\"");
1✔
366

367
        let deserialized: Errcode = serde_json::from_str(&serialized).unwrap();
1✔
368
        assert_eq!(deserialized, Errcode::Internal);
1✔
369
    }
1✔
370

371
    #[test]
372
    fn test_context_new() {
1✔
373
        let context =
1✔
374
            Context::new(Some("password"), Some("weak"), Some("strong"), Some("Password too weak"));
1✔
375

376
        assert_eq!(context.field_name, "password");
1✔
377
        assert_eq!(context.found, "weak");
1✔
378
        assert_eq!(context.expected, "strong");
1✔
379
        assert_eq!(context.message, "Password too weak");
1✔
380
    }
1✔
381

382
    #[test]
383
    fn test_context_new_with_none_values() {
1✔
384
        let context = Context::new(None, None, None, Some("General error"));
1✔
385

386
        assert!(context.field_name.is_empty());
1✔
387
        assert!(context.found.is_empty());
1✔
388
        assert!(context.expected.is_empty());
1✔
389
        assert_eq!(context.message, "General error");
1✔
390
    }
1✔
391

392
    #[test]
393
    fn test_error_into_response() {
1✔
394
        let error = Error::new(Errcode::IllegalInput, None);
1✔
395
        let response = error.into_response();
1✔
396

397
        assert_eq!(response.status(), poem::http::StatusCode::BAD_REQUEST);
1✔
398
        assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
1✔
399
    }
1✔
400

401
    #[test]
402
    fn test_error_from_sqlx_error() {
1✔
403
        use sqlx::Error as SqlxError;
404

405
        let sqlx_error = SqlxError::RowNotFound;
1✔
406
        let error: Error = sqlx_error.into();
1✔
407

408
        assert_eq!(error.code, Errcode::Internal);
1✔
409
        assert!(error.context.is_none());
1✔
410
    }
1✔
411

412
    #[test]
413
    fn test_error_into_poem_error() {
1✔
414
        let error = Error::new(Errcode::Unauthorized, None);
1✔
415
        let poem_error: poem::Error = error.into();
1✔
416

417
        // We can't directly test the poem::Error contents, but we can ensure the
418
        // conversion works
419
        assert_eq!(poem_error.status(), poem::http::StatusCode::UNAUTHORIZED);
1✔
420
    }
1✔
421

422
    #[test]
423
    fn test_errcode_display() {
1✔
424
        assert_eq!(Errcode::Internal.to_string(), "P2_CORE_INTERNAL");
1✔
425
        assert_eq!(Errcode::Unauthorized.to_string(), "P2_CORE_UNAUTHORIZED");
1✔
426
        assert_eq!(Errcode::Duplicate.to_string(), "P2_CORE_DUPLICATE");
1✔
427
        assert_eq!(Errcode::IllegalInput.to_string(), "P2_CORE_ILLEGAL_INPUT");
1✔
428
    }
1✔
429

430
    #[test]
431
    fn test_errcode_from_str() {
1✔
432
        use std::str::FromStr;
433

434
        assert_eq!(Errcode::from_str("P2_CORE_INTERNAL").unwrap(), Errcode::Internal);
1✔
435
        assert_eq!(Errcode::from_str("P2_CORE_UNAUTHORIZED").unwrap(), Errcode::Unauthorized);
1✔
436
        assert_eq!(Errcode::from_str("P2_CORE_DUPLICATE").unwrap(), Errcode::Duplicate);
1✔
437
        assert_eq!(Errcode::from_str("P2_CORE_ILLEGAL_INPUT").unwrap(), Errcode::IllegalInput);
1✔
438

439
        assert!(Errcode::from_str("INVALID_CODE").is_err());
1✔
440
    }
1✔
441
}
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