bitwarden_generators/username_forwarders/
firefox.rs

1use reqwest::{
2    header::{self},
3    StatusCode,
4};
5
6use crate::username::UsernameError;
7
8pub 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 serde_json::json;
62
63    use crate::username::UsernameError;
64
65    #[tokio::test]
66    async fn test_mock_success() {
67        use wiremock::{matchers, Mock, ResponseTemplate};
68
69        let server = wiremock::MockServer::start().await;
70
71        server
72            .register(
73                Mock::given(matchers::path("/api/v1/relayaddresses/"))
74                    .and(matchers::method("POST"))
75                    .and(matchers::header("Content-Type", "application/json"))
76                    .and(matchers::header("Authorization", "Token MY_TOKEN"))
77                    .and(matchers::body_json(json!({
78                        "enabled": true,
79                        "generated_for": "example.com",
80                        "description": "example.com - Generated by Bitwarden."
81                    })))
82                    .respond_with(ResponseTemplate::new(201).set_body_json(json!({
83                        "full_address": "[email protected]"
84                    })))
85                    .expect(1),
86            )
87            .await;
88
89        let address = super::generate_with_api_url(
90            &reqwest::Client::new(),
91            "MY_TOKEN".into(),
92            Some("example.com".into()),
93            format!("http://{}", server.address()),
94        )
95        .await
96        .unwrap();
97        assert_eq!(address, "[email protected]");
98
99        server.verify().await;
100    }
101
102    #[tokio::test]
103    async fn test_mock_without_website() {
104        use wiremock::{matchers, Mock, ResponseTemplate};
105
106        let server = wiremock::MockServer::start().await;
107
108        server
109            .register(
110                Mock::given(matchers::path("/api/v1/relayaddresses/"))
111                    .and(matchers::method("POST"))
112                    .and(matchers::header("Content-Type", "application/json"))
113                    .and(matchers::header("Authorization", "Token MY_OTHER_TOKEN"))
114                    .and(matchers::body_json(json!({
115                        "enabled": true,
116                        "description": "Generated by Bitwarden."
117                    })))
118                    .respond_with(ResponseTemplate::new(201).set_body_json(json!({
119                        "full_address": "[email protected]"
120                    })))
121                    .expect(1),
122            )
123            .await;
124
125        let address = super::generate_with_api_url(
126            &reqwest::Client::new(),
127            "MY_OTHER_TOKEN".into(),
128            None,
129            format!("http://{}", server.address()),
130        )
131        .await
132        .unwrap();
133        assert_eq!(address, "[email protected]");
134
135        server.verify().await;
136    }
137
138    #[tokio::test]
139    async fn test_mock_invalid_token() {
140        use wiremock::{matchers, Mock, ResponseTemplate};
141
142        let server = wiremock::MockServer::start().await;
143
144        server
145            .register(
146                Mock::given(matchers::path("/api/v1/relayaddresses/"))
147                    .and(matchers::method("POST"))
148                    .and(matchers::header("Content-Type", "application/json"))
149                    .and(matchers::header("Authorization", "Token MY_FAKE_TOKEN"))
150                    .and(matchers::body_json(json!({
151                        "enabled": true,
152                        "generated_for": "example.com",
153                        "description": "example.com - Generated by Bitwarden."
154                    })))
155                    .respond_with(ResponseTemplate::new(401))
156                    .expect(1),
157            )
158            .await;
159
160        let error = super::generate_with_api_url(
161            &reqwest::Client::new(),
162            "MY_FAKE_TOKEN".into(),
163            Some("example.com".into()),
164            format!("http://{}", server.address()),
165        )
166        .await
167        .unwrap_err();
168
169        assert_eq!(error.to_string(), UsernameError::InvalidApiKey.to_string());
170
171        server.verify().await;
172    }
173}