1mod error;
6
7uniffi::setup_scaffolding!("relay");
8
9pub use error::{ApiResult, Error, RelayApiError, Result};
10use error_support::handle_error;
11
12use serde::{Deserialize, Serialize};
13use url::Url;
14use viaduct::{header_names, Method, Request};
15
16#[derive(uniffi::Object)]
17pub struct RelayClient {
18 server_url: String,
19 auth_token: Option<String>,
20}
21
22#[derive(Debug, Deserialize, uniffi::Record)]
23pub struct RelayAddress {
24 pub mask_type: String,
25 pub enabled: bool,
26 pub description: String,
27 pub generated_for: String,
28 pub block_list_emails: bool,
29 pub used_on: Option<String>,
30 pub id: i64,
31 pub address: String,
32 pub domain: i64,
33 pub full_address: String,
34 pub created_at: String, pub last_modified_at: String,
36 pub last_used_at: Option<String>,
37 pub num_forwarded: i64,
38 pub num_blocked: i64,
39 pub num_level_one_trackers_blocked: i64,
40 pub num_replied: i64,
41 pub num_spam: i64,
42}
43
44#[derive(Debug, Serialize)]
45struct CreateAddressPayload<'a> {
46 enabled: bool,
47 description: &'a str,
48 generated_for: &'a str,
49 used_on: &'a str,
50}
51
52#[derive(Deserialize)]
53struct RelayApiErrorMessage {
54 detail: String,
55}
56
57impl RelayClient {
58 fn build_url(&self, path: &str) -> Result<Url> {
59 Ok(Url::parse(&format!("{}{}", self.server_url, path))?)
60 }
61
62 fn prepare_request(&self, method: Method, url: Url) -> Result<Request> {
63 log::trace!("making {} request to: {}", method.as_str(), url);
64 let mut request = Request::new(method, url);
65 if let Some(ref token) = self.auth_token {
66 request = request.header(header_names::AUTHORIZATION, format!("Bearer {}", token))?;
67 }
68 Ok(request)
69 }
70}
71
72#[uniffi::export]
73impl RelayClient {
74 #[uniffi::constructor]
75 #[handle_error(Error)]
76 pub fn new(server_url: String, auth_token: Option<String>) -> ApiResult<Self> {
77 Ok(Self {
78 server_url,
79 auth_token,
80 })
81 }
82
83 #[handle_error(Error)]
84 pub fn fetch_addresses(&self) -> ApiResult<Vec<RelayAddress>> {
85 let url = self.build_url("/api/v1/relayaddresses/")?;
86 let request = self.prepare_request(Method::Get, url)?;
87
88 let response = request.send()?;
89 let body = response.text();
90 log::trace!("response text: {}", body);
91 if let Ok(parsed) = serde_json::from_str::<RelayApiErrorMessage>(&body) {
92 return Err(Error::RelayApi(parsed.detail));
93 }
94
95 let addresses: Vec<RelayAddress> = response.json()?;
96 Ok(addresses)
97 }
98
99 #[handle_error(Error)]
100 pub fn accept_terms(&self) -> ApiResult<()> {
101 let url = self.build_url("/api/v1/terms-accepted-user/")?;
102 let request = self.prepare_request(Method::Post, url)?;
103
104 let response = request.send()?;
105 let body = response.text();
106 log::trace!("response text: {}", body);
107 if let Ok(parsed) = serde_json::from_str::<RelayApiErrorMessage>(&body) {
108 return Err(Error::RelayApi(parsed.detail));
109 }
110 Ok(())
111 }
112
113 #[handle_error(Error)]
114 pub fn create_address(
115 &self,
116 description: &str,
117 generated_for: &str,
118 used_on: &str,
119 ) -> ApiResult<RelayAddress> {
120 let url = self.build_url("/api/v1/relayaddresses/")?;
121
122 let payload = CreateAddressPayload {
123 enabled: true,
124 description,
125 generated_for,
126 used_on,
127 };
128
129 let mut request = self.prepare_request(Method::Post, url)?;
130 request = request.json(&payload);
131
132 let response = request.send()?;
133 let body = response.text();
134 log::trace!("response text: {}", body);
135 if let Ok(parsed) = serde_json::from_str::<RelayApiErrorMessage>(&body) {
136 return Err(Error::RelayApi(parsed.detail));
137 }
138
139 let address: RelayAddress = response.json()?;
140 Ok(address)
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use mockito::mock;
148
149 fn base_addresses_json(extra_fields: &str) -> String {
150 format!(
151 r#"
152 [
153 {{
154 "mask_type": "random",
155 "enabled": true,
156 "description": "Base Address",
157 "generated_for": "example.com",
158 "block_list_emails": false,
159 "id": 1,
160 "address": "base12345",
161 "domain": 2,
162 "full_address": "base12345@mozmail.com",
163 "created_at": "2021-01-01T00:00:00Z",
164 "last_modified_at": "2021-01-02T00:00:00Z",
165 {extra_fields}
166 "num_forwarded": 5,
167 "num_blocked": 1,
168 "num_level_one_trackers_blocked": 0,
169 "num_replied": 2,
170 "num_spam": 0
171 }}
172 ]
173 "#
174 )
175 }
176
177 #[test]
178 fn test_fetch_addresses() {
179 viaduct_reqwest::use_reqwest_backend();
180
181 let addresses_json = base_addresses_json(
182 r#""used_on": "example.com", "last_used_at": "2021-01-03T00:00:00Z", "#,
183 );
184 log::trace!("addresses_json: {}", addresses_json);
185
186 let _mock = mock("GET", "/api/v1/relayaddresses/")
187 .with_status(200)
188 .with_header("content-type", "application/json")
189 .with_body(addresses_json)
190 .create();
191
192 let client = RelayClient::new(mockito::server_url(), Some("mock_token".to_string()));
193
194 let addresses = client
195 .expect("success")
196 .fetch_addresses()
197 .expect("should fetch addresses");
198
199 assert_eq!(addresses.len(), 1);
200 let addr = &addresses[0];
201 assert!(addr.enabled);
202 assert_eq!(addr.full_address, "base12345@mozmail.com");
203 assert_eq!(addr.generated_for, "example.com");
204 }
205
206 #[test]
207 fn test_fetch_addresses_used_on_null() {
208 viaduct_reqwest::use_reqwest_backend();
209
210 let addresses_json =
211 base_addresses_json(r#""used_on": null,"last_used_at": "2021-01-03T00:00:00Z","#);
212
213 let _mock = mock("GET", "/api/v1/relayaddresses/")
214 .with_status(200)
215 .with_header("content-type", "application/json")
216 .with_body(addresses_json)
217 .create();
218
219 let client = RelayClient::new(mockito::server_url(), Some("mock_token".to_string()));
220 let addresses = client
221 .expect("success")
222 .fetch_addresses()
223 .expect("should fetch addresses");
224
225 assert_eq!(addresses.len(), 1);
226 assert_eq!(addresses[0].used_on, None);
227 }
228
229 #[test]
230 fn test_fetch_addresses_last_used_at_null() {
231 viaduct_reqwest::use_reqwest_backend();
232
233 let addresses_json =
234 base_addresses_json(r#""used_on": "example.com","last_used_at": null,"#);
235
236 let _mock = mock("GET", "/api/v1/relayaddresses/")
237 .with_status(200)
238 .with_header("content-type", "application/json")
239 .with_body(addresses_json)
240 .create();
241
242 let client = RelayClient::new(mockito::server_url(), Some("mock_token".to_string()));
243 let addresses = client
244 .expect("success")
245 .fetch_addresses()
246 .expect("should fetch addresses");
247
248 assert_eq!(addresses.len(), 1);
249 assert_eq!(addresses[0].last_used_at, None);
250 }
251
252 fn test_accept_terms_response(
253 status_code: usize,
254 body: Option<&str>,
255 token: Option<&str>,
256 expect_error: bool,
257 ) {
258 viaduct_reqwest::use_reqwest_backend();
259
260 let mut mock = mock("POST", "/api/v1/terms-accepted-user/").with_status(status_code);
261
262 if let Some(body_text) = body {
263 mock = mock
264 .with_header("content-type", "application/json")
265 .with_body(body_text);
266 }
267
268 let _mock = mock.create();
269 let client = RelayClient::new(mockito::server_url(), token.map(String::from));
270
271 let result = client.expect("success").accept_terms();
272
273 if expect_error {
274 assert!(result.is_err(), "Expected error but got success.");
275 } else {
276 assert!(result.is_ok(), "Expected success but got error.");
277 }
278 }
279
280 #[test]
281 fn test_accept_terms_user_created() {
282 test_accept_terms_response(201, None, Some("mock_token"), false);
283 }
284
285 #[test]
286 fn test_accept_terms_user_exists() {
287 test_accept_terms_response(202, None, Some("mock_token"), false);
288 }
289
290 #[test]
291 fn test_accept_terms_missing_authorization_header() {
292 test_accept_terms_response(
293 400,
294 Some(r#"{"detail": "Missing Bearer header."}"#),
295 None,
296 true,
297 );
298 }
299
300 #[test]
301 fn test_accept_terms_invalid_token() {
302 test_accept_terms_response(
303 403,
304 Some(r#"{"detail": "Invalid token."}"#),
305 Some("invalid_token"),
306 true,
307 );
308 }
309
310 #[test]
311 fn test_accept_terms_server_error_profile_failure() {
312 test_accept_terms_response(
313 500,
314 Some(r#"{"detail": "Did not receive a 200 response for account profile."}"#),
315 Some("valid_token_but_profile_fails"),
316 true,
317 );
318 }
319
320 #[test]
321 fn test_accept_terms_user_not_found() {
322 test_accept_terms_response(
323 404,
324 Some(r#"{"detail": "FXA user not found."}"#),
325 Some("valid_token_but_user_missing"),
326 true,
327 );
328 }
329
330 #[test]
331 fn test_create_address() {
332 viaduct_reqwest::use_reqwest_backend();
333
334 let address_json = r#"
335 {
336 "mask_type": "alias",
337 "enabled": true,
338 "description": "Created Address",
339 "generated_for": "example.com",
340 "block_list_emails": false,
341 "used_on": "example.com",
342 "id": 2,
343 "address": "new123456",
344 "domain": 2,
345 "full_address": "new123456@mozmail.com",
346 "created_at": "2021-01-04T00:00:00Z",
347 "last_modified_at": "2021-01-05T00:00:00Z",
348 "last_used_at": "2021-01-06T00:00:00Z",
349 "num_forwarded": 3,
350 "num_blocked": 0,
351 "num_level_one_trackers_blocked": 0,
352 "num_replied": 1,
353 "num_spam": 0
354 }
355 "#;
356
357 let _mock = mock("POST", "/api/v1/relayaddresses/")
358 .match_header("authorization", "Bearer mock_token")
359 .match_header("content-type", "application/json")
360 .with_status(201)
361 .with_header("content-type", "application/json")
362 .with_body(address_json)
363 .create();
364
365 let client = RelayClient::new(mockito::server_url(), Some("mock_token".to_string()));
366
367 let address = client
368 .expect("success")
369 .create_address("Created Address", "example.com", "example.com")
370 .expect("should create address successfully");
371
372 assert_eq!(address.full_address, "new123456@mozmail.com");
373 assert_eq!(address.generated_for, "example.com");
374 assert!(address.enabled);
375 }
376}