bitwarden_generators/username_forwarders/
firefox.rs1use 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 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}