ddclient_rs/
errors.rs

1// Copyright (c) 2023, Direct Decisions Rust client AUTHORS.
2// All rights reserved.
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file.
5
6use serde::Deserialize;
7use thiserror::Error;
8
9/// Represents an error returned by the API.
10///
11/// This enum represents an error returned by the API. It contains various error types that can be
12/// defined at https://api.directdecisions.com/v1.
13///
14/// Client errors represent errors that occur on the client side.
15///
16#[derive(Debug, Error)]
17pub enum ApiError {
18    #[error("Bad Request: {0:?}")]
19    BadRequest(Vec<BadRequestError>),
20
21    #[error("Unauthorized")]
22    Unauthorized,
23
24    #[error("Not Found")]
25    NotFound,
26
27    #[error("Forbidden")]
28    Forbidden,
29
30    #[error("Internal Server Error: {0}")]
31    InternalServerError(String),
32
33    #[error("Method Not Allowed")]
34    MethodNotAllowed,
35
36    #[error("Too many requests")]
37    TooManyRequests,
38
39    #[error("Other Error: {0}")]
40    Other(String),
41
42    #[error("Client Error: {0}")]
43    Client(#[from] ClientError),
44}
45
46/// Represents a client error.
47///
48/// This enum represents a client error, such as a bad gateway or service unavailable error.
49///
50/// It also includes an HTTP request error variant that wraps the reqwest::Error type.
51#[derive(Debug, Error)]
52pub enum ClientError {
53    #[error("Bad Gateway")]
54    BadGateway,
55    #[error("HTTP Request Error: {0}")]
56    HttpRequestError(#[from] reqwest::Error),
57
58    #[error("Service Unavailable")]
59    ServiceUnavailable,
60}
61
62/// Represents a bad request error.
63#[derive(Error, Debug, Deserialize, PartialEq)]
64pub enum BadRequestError {
65    #[error("Invalid data")]
66    InvalidData,
67    #[error("Missing choices")]
68    MissingChoices,
69    #[error("Choice too long")]
70    ChoiceTooLong,
71    #[error("Too many choices")]
72    TooManyChoices,
73    #[error("Choice required")]
74    ChoiceRequired,
75    #[error("Ballot required")]
76    BallotRequired,
77    #[error("Voter ID too long")]
78    VoterIDTooLong,
79    #[error("Invalid voter ID")]
80    InvalidVoterID,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::handle_api_response;
87    use http::response::Builder;
88    use reqwest::{Response, StatusCode};
89
90    impl PartialEq for ApiError {
91        fn eq(&self, other: &Self) -> bool {
92            match (self, other) {
93                (ApiError::BadRequest(errors_self), ApiError::BadRequest(errors_other)) => {
94                    for err in errors_self {
95                        if !errors_other.contains(err) {
96                            return false;
97                        }
98                    }
99
100                    true
101                }
102                (ApiError::Unauthorized, ApiError::Unauthorized) => true,
103                (ApiError::NotFound, ApiError::NotFound) => true,
104                (ApiError::Forbidden, ApiError::Forbidden) => true,
105                (
106                    ApiError::InternalServerError(msg_self),
107                    ApiError::InternalServerError(msg_other),
108                ) => msg_self == msg_other,
109                (ApiError::MethodNotAllowed, ApiError::MethodNotAllowed) => true,
110                (ApiError::TooManyRequests, ApiError::TooManyRequests) => true,
111                (ApiError::Other(msg_self), ApiError::Other(msg_other)) => msg_self == msg_other,
112                (ApiError::Client(err_self), ApiError::Client(err_other)) => {
113                    match (err_self, err_other) {
114                        (ClientError::BadGateway, ClientError::BadGateway) => true,
115                        (ClientError::ServiceUnavailable, ClientError::ServiceUnavailable) => true,
116                        (
117                            ClientError::HttpRequestError(err_self),
118                            ClientError::HttpRequestError(err_other),
119                        ) => err_self.to_string() == err_other.to_string(),
120                        _ => false,
121                    }
122                }
123                _ => false,
124            }
125        }
126    }
127
128    fn create_mock_response(status: StatusCode, body: &str) -> Response {
129        let response = Builder::new()
130            .status(status)
131            .body(body.to_string())
132            .unwrap();
133        Response::from(response)
134    }
135
136    #[tokio::test]
137    async fn api_errors_test() {
138        let test_cases = vec![
139            (
140                StatusCode::BAD_GATEWAY,
141                "",
142                ApiError::Client(ClientError::BadGateway),
143            ),
144            (
145                StatusCode::SERVICE_UNAVAILABLE,
146                "",
147                ApiError::Client(ClientError::ServiceUnavailable),
148            ),
149            (StatusCode::BAD_REQUEST, "", ApiError::BadRequest(vec![])),
150            (
151                StatusCode::BAD_REQUEST,
152                r#"{"code":400,"message":"Bad Request","errors":["Invalid data"]}"#,
153                ApiError::BadRequest(vec![BadRequestError::InvalidData]),
154            ),
155            (
156                StatusCode::BAD_REQUEST,
157                r#"{"code":400,"message":"Bad Request","errors":["Missing choices"]}"#,
158                ApiError::BadRequest(vec![BadRequestError::MissingChoices]),
159            ),
160            (
161                StatusCode::BAD_REQUEST,
162                r#"{"code":400,"message":"Bad Request","errors":["Choice too long"]}"#,
163                ApiError::BadRequest(vec![BadRequestError::ChoiceTooLong]),
164            ),
165            (
166                StatusCode::BAD_REQUEST,
167                r#"{"code":400,"message":"Bad Request","errors":["Too many choices"]}"#,
168                ApiError::BadRequest(vec![BadRequestError::TooManyChoices]),
169            ),
170            (
171                StatusCode::BAD_REQUEST,
172                r#"{"code":400,"message":"Bad Request","errors":["Choice required"]}"#,
173                ApiError::BadRequest(vec![BadRequestError::ChoiceRequired]),
174            ),
175            (
176                StatusCode::BAD_REQUEST,
177                r#"{"code":400,"message":"Bad Request","errors":["Ballot required"]}"#,
178                ApiError::BadRequest(vec![BadRequestError::BallotRequired]),
179            ),
180            (
181                StatusCode::BAD_REQUEST,
182                r#"{"code":400,"message":"Bad Request","errors":["Voter ID too long"]}"#,
183                ApiError::BadRequest(vec![BadRequestError::VoterIDTooLong]),
184            ),
185            (
186                StatusCode::BAD_REQUEST,
187                r#"{"code":400,"message":"Bad Request","errors":["Invalid voter ID"]}"#,
188                ApiError::BadRequest(vec![BadRequestError::InvalidVoterID]),
189            ),
190            (
191                StatusCode::BAD_REQUEST,
192                r#"{"code":400,"message":"Bad Request","errors":["Invalid data","Missing choices"]}"#,
193                ApiError::BadRequest(vec![
194                    BadRequestError::InvalidData,
195                    BadRequestError::MissingChoices,
196                ]),
197            ),
198            (StatusCode::TOO_MANY_REQUESTS, "", ApiError::TooManyRequests),
199            (
200                StatusCode::INTERNAL_SERVER_ERROR,
201                "Internal Server Error",
202                ApiError::InternalServerError("Internal Server Error".to_string()),
203            ),
204            (
205                StatusCode::INTERNAL_SERVER_ERROR,
206                "",
207                ApiError::InternalServerError("".to_string()),
208            ),
209            (StatusCode::NOT_FOUND, "", ApiError::NotFound),
210            (StatusCode::UNAUTHORIZED, "", ApiError::Unauthorized),
211            (StatusCode::FORBIDDEN, "", ApiError::Forbidden),
212            (
213                StatusCode::METHOD_NOT_ALLOWED,
214                "",
215                ApiError::MethodNotAllowed,
216            ),
217        ];
218
219        for (status, body, expected_error) in test_cases {
220            let mock_response = create_mock_response(status, body);
221            let result = handle_api_response::<()>(mock_response).await;
222
223            match result {
224                Ok(_) => assert!(false, "Expected error but got Ok"),
225                Err(err) => assert_eq!(err, expected_error),
226            }
227        }
228    }
229}