Skip to main content

bitwarden_generators/username_forwarders/
firefox.rs

1use reqwest::{
2    StatusCode,
3    header::{self},
4};
5
6use crate::username::UsernameError;
7
8pub(crate) async fn generate(
9    http: &reqwest::Client,
10    api_token: String,
11    website: Option<String>,
12) -> Result<String, UsernameError> {
13    generate_with_api_url(http, api_token, website, "https://relay.firefox.com".into()).await
14}
15
16async fn generate_with_api_url(
17    http: &reqwest::Client,
18    api_token: String,
19    website: Option<String>,
20    api_url: String,
21) -> Result<String, UsernameError> {
22    #[derive(serde::Serialize)]
23    struct Request {
24        enabled: bool,
25        #[serde(skip_serializing_if = "Option::is_none")]
26        generated_for: Option<String>,
27        description: String,
28    }
29
30    let description = super::format_description_ff(&website);
31
32    let response = http
33        .post(format!("{api_url}/api/v1/relayaddresses/"))
34        .header(header::AUTHORIZATION, format!("Token {api_token}"))
35        .json(&Request {
36            enabled: true,
37            generated_for: website,
38            description,
39        })
40        .send()
41        .await?;
42
43    if response.status() == StatusCode::UNAUTHORIZED {
44        return Err(UsernameError::InvalidApiKey);
45    }
46
47    // Throw any other errors
48    response.error_for_status_ref()?;
49
50    #[derive(serde::Deserialize)]
51    struct Response {
52        full_address: String,
53    }
54    let response: Response = response.json().await?;
55
56    Ok(response.full_address)
57}
58
59#[cfg(test)]
60mod tests {
61    use bitwarden_api_base::new_http_client;
62    use serde_json::json;
63
64    use crate::username::UsernameError;
65
66    #[tokio::test]
67    async fn test_mock_success() {
68        use wiremock::{Mock, ResponseTemplate, matchers};
69
70        let server = wiremock::MockServer::start().await;
71
72        server
73            .register(
74                Mock::given(matchers::path("/api/v1/relayaddresses/"))
75                    .and(matchers::method("POST"))
76                    .and(matchers::header("Content-Type", "application/json"))
77                    .and(matchers::header("Authorization", "Token MY_TOKEN"))
78                    .and(matchers::body_json(json!({
79                        "enabled": true,
80                        "generated_for": "example.com",
81                        "description": "example.com - Generated by Bitwarden."
82                    })))
83                    .respond_with(ResponseTemplate::new(201).set_body_json(json!({
84                        "full_address": "[email protected]"
85                    })))
86                    .expect(1),
87            )
88            .await;
89
90        let address = super::generate_with_api_url(
91            &new_http_client(),
92            "MY_TOKEN".into(),
93            Some("example.com".into()),
94            format!("http://{}", server.address()),
95        )
96        .await
97        .unwrap();
98        assert_eq!(address, "[email protected]");
99
100        server.verify().await;
101    }
102
103    #[tokio::test]
104    async fn test_mock_without_website() {
105        use wiremock::{Mock, ResponseTemplate, matchers};
106
107        let server = wiremock::MockServer::start().await;
108
109        server
110            .register(
111                Mock::given(matchers::path("/api/v1/relayaddresses/"))
112                    .and(matchers::method("POST"))
113                    .and(matchers::header("Content-Type", "application/json"))
114                    .and(matchers::header("Authorization", "Token MY_OTHER_TOKEN"))
115                    .and(matchers::body_json(json!({
116                        "enabled": true,
117                        "description": "Generated by Bitwarden."
118                    })))
119                    .respond_with(ResponseTemplate::new(201).set_body_json(json!({
120                        "full_address": "[email protected]"
121                    })))
122                    .expect(1),
123            )
124            .await;
125
126        let address = super::generate_with_api_url(
127            &new_http_client(),
128            "MY_OTHER_TOKEN".into(),
129            None,
130            format!("http://{}", server.address()),
131        )
132        .await
133        .unwrap();
134        assert_eq!(address, "[email protected]");
135
136        server.verify().await;
137    }
138
139    #[tokio::test]
140    async fn test_mock_invalid_token() {
141        use wiremock::{Mock, ResponseTemplate, matchers};
142
143        let server = wiremock::MockServer::start().await;
144
145        server
146            .register(
147                Mock::given(matchers::path("/api/v1/relayaddresses/"))
148                    .and(matchers::method("POST"))
149                    .and(matchers::header("Content-Type", "application/json"))
150                    .and(matchers::header("Authorization", "Token MY_FAKE_TOKEN"))
151                    .and(matchers::body_json(json!({
152                        "enabled": true,
153                        "generated_for": "example.com",
154                        "description": "example.com - Generated by Bitwarden."
155                    })))
156                    .respond_with(ResponseTemplate::new(401))
157                    .expect(1),
158            )
159            .await;
160
161        let error = super::generate_with_api_url(
162            &new_http_client(),
163            "MY_FAKE_TOKEN".into(),
164            Some("example.com".into()),
165            format!("http://{}", server.address()),
166        )
167        .await
168        .unwrap_err();
169
170        assert_eq!(error.to_string(), UsernameError::InvalidApiKey.to_string());
171
172        server.verify().await;
173    }
174}