bitwarden_generators/username_forwarders/
addyio.rs

1use reqwest::{header::CONTENT_TYPE, StatusCode};
2
3use crate::username::UsernameError;
4
5pub 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 serde_json::json;
55
56    use crate::username::UsernameError;
57    #[tokio::test]
58    async fn test_mock_server() {
59        use wiremock::{matchers, Mock, ResponseTemplate};
60
61        let server = wiremock::MockServer::start().await;
62
63        // Mock the request to the addy.io API, and verify that the correct request is made
64        server
65            .register(
66                Mock::given(matchers::path("/api/v1/aliases"))
67                    .and(matchers::method("POST"))
68                    .and(matchers::header("Content-Type", "application/json"))
69                    .and(matchers::header("Authorization", "Bearer MY_TOKEN"))
70                    .and(matchers::body_json(json!({
71                        "domain": "myemail.com",
72                        "description": "Website: example.com. Generated by Bitwarden."
73                    })))
74                    .respond_with(ResponseTemplate::new(201).set_body_json(json!({
75                        "data": {
76                            "id": "50c9e585-e7f5-41c4-9016-9014c15454bc",
77                            "user_id": "ca0a4e09-c266-4f6f-845c-958db5090f09",
78                            "local_part": "50c9e585-e7f5-41c4-9016-9014c15454bc",
79                            "domain": "myemail.com",
80                            "email": "[email protected]",
81                            "active": true
82                        }
83                    })))
84                    .expect(1),
85            )
86            .await;
87        // Mock an invalid API token request
88        server
89            .register(
90                Mock::given(matchers::path("/api/v1/aliases"))
91                    .and(matchers::method("POST"))
92                    .and(matchers::header("Content-Type", "application/json"))
93                    .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN"))
94                    .and(matchers::body_json(json!({
95                        "domain": "myemail.com",
96                        "description": "Website: example.com. Generated by Bitwarden."
97                    })))
98                    .respond_with(ResponseTemplate::new(401))
99                    .expect(1),
100            )
101            .await;
102        // Mock an invalid domain
103        server
104            .register(
105                Mock::given(matchers::path("/api/v1/aliases"))
106                    .and(matchers::method("POST"))
107                    .and(matchers::header("Content-Type", "application/json"))
108                    .and(matchers::header("Authorization", "Bearer MY_TOKEN"))
109                    .and(matchers::body_json(json!({
110                        "domain": "gmail.com",
111                        "description": "Website: example.com. Generated by Bitwarden."
112                    })))
113                    .respond_with(ResponseTemplate::new(403))
114                    .expect(1),
115            )
116            .await;
117
118        let address = super::generate(
119            &reqwest::Client::new(),
120            "MY_TOKEN".into(),
121            "myemail.com".into(),
122            format!("http://{}", server.address()),
123            Some("example.com".into()),
124        )
125        .await
126        .unwrap();
127
128        let fake_token_error = super::generate(
129            &reqwest::Client::new(),
130            "MY_FAKE_TOKEN".into(),
131            "myemail.com".into(),
132            format!("http://{}", server.address()),
133            Some("example.com".into()),
134        )
135        .await
136        .unwrap_err();
137
138        assert_eq!(
139            fake_token_error.to_string(),
140            UsernameError::InvalidApiKey.to_string()
141        );
142
143        let fake_domain_error = super::generate(
144            &reqwest::Client::new(),
145            "MY_TOKEN".into(),
146            "gmail.com".into(),
147            format!("http://{}", server.address()),
148            Some("example.com".into()),
149        )
150        .await
151        .unwrap_err();
152
153        assert!(fake_domain_error.to_string().contains("403 Forbidden"));
154
155        server.verify().await;
156        assert_eq!(address, "[email protected]");
157    }
158}