Skip to main content

bitwarden_generators/username_forwarders/
addyio.rs

1use reqwest::{StatusCode, header::CONTENT_TYPE};
2
3use crate::username::UsernameError;
4
5pub(crate) async fn generate(
6    http: &reqwest::Client,
7    api_token: String,
8    domain: String,
9    base_url: String,
10    website: Option<String>,
11) -> Result<String, UsernameError> {
12    let description = super::format_description(&website);
13
14    #[derive(serde::Serialize)]
15    struct Request {
16        domain: String,
17        description: String,
18    }
19
20    let response = http
21        .post(format!("{base_url}/api/v1/aliases"))
22        .header(CONTENT_TYPE, "application/json")
23        .bearer_auth(api_token)
24        .header("X-Requested-With", "XMLHttpRequest")
25        .json(&Request {
26            domain,
27            description,
28        })
29        .send()
30        .await?;
31
32    if response.status() == StatusCode::UNAUTHORIZED {
33        return Err(UsernameError::InvalidApiKey);
34    }
35
36    // Throw any other errors
37    response.error_for_status_ref()?;
38
39    #[derive(serde::Deserialize)]
40    struct ResponseData {
41        email: String,
42    }
43    #[derive(serde::Deserialize)]
44    struct Response {
45        data: ResponseData,
46    }
47    let response: Response = response.json().await?;
48
49    Ok(response.data.email)
50}
51
52#[cfg(test)]
53mod tests {
54    use bitwarden_api_base::new_http_client;
55    use serde_json::json;
56
57    use crate::username::UsernameError;
58    #[tokio::test]
59    async fn test_mock_server() {
60        use wiremock::{Mock, ResponseTemplate, matchers};
61
62        let server = wiremock::MockServer::start().await;
63
64        // Mock the request to the addy.io API, and verify that the correct request is made
65        server
66            .register(
67                Mock::given(matchers::path("/api/v1/aliases"))
68                    .and(matchers::method("POST"))
69                    .and(matchers::header("Content-Type", "application/json"))
70                    .and(matchers::header("Authorization", "Bearer MY_TOKEN"))
71                    .and(matchers::body_json(json!({
72                        "domain": "myemail.com",
73                        "description": "Website: example.com. Generated by Bitwarden."
74                    })))
75                    .respond_with(ResponseTemplate::new(201).set_body_json(json!({
76                        "data": {
77                            "id": "50c9e585-e7f5-41c4-9016-9014c15454bc",
78                            "user_id": "ca0a4e09-c266-4f6f-845c-958db5090f09",
79                            "local_part": "50c9e585-e7f5-41c4-9016-9014c15454bc",
80                            "domain": "myemail.com",
81                            "email": "[email protected]",
82                            "active": true
83                        }
84                    })))
85                    .expect(1),
86            )
87            .await;
88        // Mock an invalid API token request
89        server
90            .register(
91                Mock::given(matchers::path("/api/v1/aliases"))
92                    .and(matchers::method("POST"))
93                    .and(matchers::header("Content-Type", "application/json"))
94                    .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN"))
95                    .and(matchers::body_json(json!({
96                        "domain": "myemail.com",
97                        "description": "Website: example.com. Generated by Bitwarden."
98                    })))
99                    .respond_with(ResponseTemplate::new(401))
100                    .expect(1),
101            )
102            .await;
103        // Mock an invalid domain
104        server
105            .register(
106                Mock::given(matchers::path("/api/v1/aliases"))
107                    .and(matchers::method("POST"))
108                    .and(matchers::header("Content-Type", "application/json"))
109                    .and(matchers::header("Authorization", "Bearer MY_TOKEN"))
110                    .and(matchers::body_json(json!({
111                        "domain": "gmail.com",
112                        "description": "Website: example.com. Generated by Bitwarden."
113                    })))
114                    .respond_with(ResponseTemplate::new(403))
115                    .expect(1),
116            )
117            .await;
118
119        let address = super::generate(
120            &new_http_client(),
121            "MY_TOKEN".into(),
122            "myemail.com".into(),
123            format!("http://{}", server.address()),
124            Some("example.com".into()),
125        )
126        .await
127        .unwrap();
128
129        let fake_token_error = super::generate(
130            &new_http_client(),
131            "MY_FAKE_TOKEN".into(),
132            "myemail.com".into(),
133            format!("http://{}", server.address()),
134            Some("example.com".into()),
135        )
136        .await
137        .unwrap_err();
138
139        assert_eq!(
140            fake_token_error.to_string(),
141            UsernameError::InvalidApiKey.to_string()
142        );
143
144        let fake_domain_error = super::generate(
145            &new_http_client(),
146            "MY_TOKEN".into(),
147            "gmail.com".into(),
148            format!("http://{}", server.address()),
149            Some("example.com".into()),
150        )
151        .await
152        .unwrap_err();
153
154        assert!(fake_domain_error.to_string().contains("403 Forbidden"));
155
156        server.verify().await;
157        assert_eq!(address, "[email protected]");
158    }
159}