1use crate::*;
19use async_trait::async_trait;
20use bytes::Buf;
21use endbasic_std::console::remove_control_chars;
22use reqwest::header::HeaderMap;
23use reqwest::Response;
24use reqwest::StatusCode;
25use std::cell::RefCell;
26use std::io;
27use std::rc::Rc;
28use std::str;
29use url::Url;
30
31async fn http_response_to_io_error(response: Response) -> io::Error {
33 let status = response.status();
34
35 let kind = match status {
36 StatusCode::OK => panic!("Should not have been called on a successful request"),
37
38 StatusCode::BAD_REQUEST => io::ErrorKind::InvalidInput,
40 StatusCode::FORBIDDEN => io::ErrorKind::PermissionDenied,
41 StatusCode::INSUFFICIENT_STORAGE => io::ErrorKind::Other,
42 StatusCode::INTERNAL_SERVER_ERROR => io::ErrorKind::Other,
43 StatusCode::NOT_FOUND => io::ErrorKind::NotFound,
44 StatusCode::PAYLOAD_TOO_LARGE => io::ErrorKind::InvalidInput,
45 StatusCode::SERVICE_UNAVAILABLE => io::ErrorKind::AddrNotAvailable,
46 StatusCode::UNAUTHORIZED => io::ErrorKind::PermissionDenied,
47
48 _ => io::ErrorKind::Other,
49 };
50
51 match response.text().await {
52 Ok(text) => match serde_json::from_str::<ErrorResponse>(&text) {
53 Ok(response) => io::Error::new(
54 kind,
55 format!("{} (server code: {})", remove_control_chars(response.message), status),
56 ),
57 _ => io::Error::new(
58 kind,
59 format!(
60 "HTTP request returned status {} with text '{}'",
61 status,
62 remove_control_chars(text)
63 ),
64 ),
65 },
66 Err(e) => io::Error::new(
67 kind,
68 format!(
69 "HTTP request returned status {} and failed to get text due to {}",
70 status,
71 remove_control_chars(e.to_string())
72 ),
73 ),
74 }
75}
76
77fn reqwest_error_to_io_error(e: reqwest::Error) -> io::Error {
79 io::Error::new(io::ErrorKind::Other, format!("{}", e))
80}
81
82struct AuthData {
84 username: String,
85 access_token: AccessToken,
86}
87
88#[cfg_attr(test, derive(Clone))]
90pub struct CloudService {
91 api_address: Url,
92 client: reqwest::Client,
93 auth_data: Rc<RefCell<Option<AuthData>>>,
94}
95
96impl CloudService {
97 pub fn new(api_address: &str) -> io::Result<Self> {
99 let url = match Url::parse(api_address) {
100 Ok(url) => url,
101 Err(e) => {
102 return Err(io::Error::new(
103 io::ErrorKind::InvalidInput,
104 format!("Invalid base API address: {}", e),
105 ))
106 }
107 };
108
109 if !(url.path().is_empty() || url.path() == "/") {
110 return Err(io::Error::new(
111 io::ErrorKind::InvalidInput,
112 "Invalid base API address: cannot contain a path".to_owned(),
113 ));
114 }
115
116 let auth_data = Rc::from(RefCell::from(None));
117
118 Ok(Self { api_address: url, client: reqwest::Client::default(), auth_data })
119 }
120
121 fn make_url(&self, path: &str) -> Url {
123 assert!(path.starts_with("api/"));
124 let mut url = self.api_address.clone();
125 assert!(url.path().is_empty() || url.path() == "/");
126 url.set_path(path);
127 url
128 }
129
130 fn default_headers(&self) -> HeaderMap {
132 let mut headers = HeaderMap::new();
133 headers.insert(
134 "x-endbasic-client-version",
135 env!("CARGO_PKG_VERSION")
136 .parse()
137 .expect("Package version should have been serializable"),
138 );
139 headers
140 }
141
142 fn require_auth_data(data: Option<&AuthData>) -> io::Result<&AuthData> {
145 match data.as_ref() {
146 Some(data) => Ok(data),
147 None => {
148 Err(io::Error::new(io::ErrorKind::PermissionDenied, "Not logged in yet".to_owned()))
149 }
150 }
151 }
152}
153
154#[async_trait(?Send)]
155impl Service for CloudService {
156 async fn signup(&mut self, request: &SignupRequest) -> io::Result<()> {
157 let response = self
158 .client
159 .post(self.make_url("api/signup"))
160 .headers(self.default_headers())
161 .header("Content-Type", "application/json")
162 .body(serde_json::to_vec(&request)?)
163 .send()
164 .await
165 .map_err(reqwest_error_to_io_error)?;
166 match response.status() {
167 StatusCode::OK => Ok(()),
168 _ => Err(http_response_to_io_error(response).await),
169 }
170 }
171
172 async fn login(&mut self, username: &str, password: &str) -> io::Result<LoginResponse> {
173 let basic_auth =
176 format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", username, password)));
177
178 let response = self
179 .client
180 .post(self.make_url("api/login"))
181 .headers(self.default_headers())
182 .header("Authorization", basic_auth)
183 .header("Content-Length", 0)
184 .send()
185 .await
186 .map_err(reqwest_error_to_io_error)?;
187 match response.status() {
188 StatusCode::OK => {
189 let bytes = response.bytes().await.map_err(reqwest_error_to_io_error)?;
190 let response: LoginResponse = serde_json::from_reader(bytes.reader())?;
191 let auth_data = AuthData {
192 username: username.to_owned(),
193 access_token: response.access_token.clone(),
194 };
195 *(self.auth_data.borrow_mut()) = Some(auth_data);
196 Ok(response)
197 }
198 _ => Err(http_response_to_io_error(response).await),
199 }
200 }
201
202 async fn logout(&mut self) -> io::Result<()> {
203 let mut auth_data = self.auth_data.borrow_mut();
204 let response = {
205 let auth_data = Self::require_auth_data(auth_data.as_ref())?;
206 self.client
207 .post(self.make_url(&format!("api/users/{}/logout", auth_data.username)))
208 .headers(self.default_headers())
209 .header("Content-Length", 0)
210 .bearer_auth(auth_data.access_token.as_str())
211 .send()
212 .await
213 .map_err(reqwest_error_to_io_error)?
214 };
215 match response.status() {
216 StatusCode::OK => {
217 *auth_data = None;
218 Ok(())
219 }
220 _ => Err(http_response_to_io_error(response).await),
221 }
222 }
223
224 fn is_logged_in(&self) -> bool {
225 self.auth_data.borrow().is_some()
226 }
227
228 fn logged_in_username(&self) -> Option<String> {
229 self.auth_data.borrow().as_ref().map(|x| x.username.to_owned())
230 }
231
232 async fn get_files(&mut self, username: &str) -> io::Result<GetFilesResponse> {
233 let mut builder = self
234 .client
235 .get(self.make_url(&format!("api/users/{}/files", username)))
236 .headers(self.default_headers());
237 if let Some(auth_data) = self.auth_data.borrow().as_ref() {
238 builder = builder.bearer_auth(auth_data.access_token.as_str());
239 }
240 let response = builder.send().await.map_err(reqwest_error_to_io_error)?;
241 match response.status() {
242 StatusCode::OK => {
243 let bytes = response.bytes().await.map_err(reqwest_error_to_io_error)?;
244 let response: GetFilesResponse = serde_json::from_reader(bytes.reader())?;
245 Ok(response)
246 }
247 _ => Err(http_response_to_io_error(response).await),
248 }
249 }
250
251 async fn get_file(
252 &mut self,
253 username: &str,
254 filename: &str,
255 request: &GetFileRequest,
256 ) -> io::Result<GetFileResponse> {
257 let mut builder = self
258 .client
259 .get(self.make_url(&format!("api/users/{}/files/{}", username, filename)))
260 .headers(self.default_headers())
261 .query(&request);
262 if let Some(auth_data) = self.auth_data.borrow().as_ref() {
263 builder = builder.bearer_auth(auth_data.access_token.as_str());
264 }
265 let response = builder.send().await.map_err(reqwest_error_to_io_error)?;
266 match response.status() {
267 StatusCode::OK => {
268 let bytes = response.bytes().await.map_err(reqwest_error_to_io_error)?;
269 let response: GetFileResponse = serde_json::from_reader(bytes.reader())?;
270 Ok(response)
271 }
272 _ => Err(http_response_to_io_error(response).await),
273 }
274 }
275
276 async fn patch_file(
277 &mut self,
278 username: &str,
279 filename: &str,
280 request: &PatchFileRequest,
281 ) -> io::Result<()> {
282 let auth_data = self.auth_data.borrow();
283
284 let response = self
285 .client
286 .patch(self.make_url(&format!("api/users/{}/files/{}", username, filename)))
287 .headers(self.default_headers())
288 .header("Content-Type", "application/json")
289 .body(serde_json::to_vec(&request)?)
290 .bearer_auth(Self::require_auth_data(auth_data.as_ref())?.access_token.as_str())
291 .send()
292 .await
293 .map_err(reqwest_error_to_io_error)?;
294 match response.status() {
295 StatusCode::OK | StatusCode::CREATED => Ok(()),
296 _ => Err(http_response_to_io_error(response).await),
297 }
298 }
299
300 async fn delete_file(&mut self, username: &str, filename: &str) -> io::Result<()> {
301 let auth_data = self.auth_data.borrow();
302
303 let response = self
304 .client
305 .delete(self.make_url(&format!("api/users/{}/files/{}", username, filename)))
306 .headers(self.default_headers())
307 .header("Content-Length", 0)
308 .bearer_auth(Self::require_auth_data(auth_data.as_ref())?.access_token.as_str())
309 .send()
310 .await
311 .map_err(reqwest_error_to_io_error)?;
312 match response.status() {
313 StatusCode::OK => Ok(()),
314 _ => Err(http_response_to_io_error(response).await),
315 }
316 }
317}
318
319#[cfg(test)]
320mod testutils {
321 use super::*;
322 use std::env;
323
324 pub(crate) fn new_service_from_env() -> CloudService {
326 let service_api = env::var("SERVICE_URL").expect("Expected env config not found");
327 CloudService::new(&service_api).unwrap()
328 }
329
330 pub(crate) struct TestContext {
332 service: CloudService,
333
334 username: Option<String>,
336 files_to_delete: Vec<String>,
337 }
338
339 impl TestContext {
340 pub(crate) fn new_from_env() -> Self {
342 TestContext { service: new_service_from_env(), username: None, files_to_delete: vec![] }
343 }
344
345 pub(crate) fn service(&self) -> CloudService {
347 self.service.clone()
348 }
349
350 pub(crate) fn get_username(&self, i: u8) -> String {
352 env::var(format!("TEST_ACCOUNT_{}_USERNAME", i)).expect("Expected env config not found")
353 }
354
355 pub(crate) async fn do_login(&mut self, i: u8) -> String {
361 let username = self.get_username(i);
362 let password = env::var(format!("TEST_ACCOUNT_{}_PASSWORD", i))
363 .expect("Expected env config not found");
364 let _response = self.service.login(&username, &password).await.unwrap();
365 self.username = Some(username.clone());
366 username
367 }
368
369 pub(crate) async fn do_logout(&mut self) {
371 self.service.logout().await.unwrap();
372 self.username = None;
373 }
374
375 pub(crate) fn random_file(&mut self) -> (String, String) {
378 let filename = format!("file-{}", rand::random::<u64>());
379 let content = format!("Test content for {}", filename);
380 self.files_to_delete.push(filename.clone());
381 (filename, content)
382 }
383 }
384
385 impl Drop for TestContext {
386 fn drop(&mut self) {
387 #[tokio::main]
388 #[allow(clippy::single_match)]
389 async fn cleanup(context: &mut TestContext) {
390 match context.username.as_ref() {
391 Some(username) => {
392 for filename in context.files_to_delete.iter() {
393 if let Err(e) = context.service.delete_file(username, filename).await {
394 eprintln!(
395 "Failed to delete file {} during cleanup: {}",
396 filename, e
397 );
398 }
399 }
400
401 if let Err(e) = context.service.logout().await {
402 eprintln!("Failed to log out for {} during cleanup: {}", username, e);
403 }
404 }
405 _ => {
406 }
410 }
411 }
412 cleanup(self);
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::testutils::*;
428 use super::*;
429 use std::env;
430
431 #[test]
432 #[ignore = "Requires environment configuration and is expensive"]
433 fn test_login_ok() {
434 #[tokio::main]
435 async fn run(context: &mut TestContext) {
436 let _username = context.do_login(1).await;
437 }
438 run(&mut TestContext::new_from_env());
439 }
440
441 #[tokio::test]
442 #[ignore = "Requires environment configuration and is expensive"]
443 async fn test_login_bad_password() {
444 let username = env::var("TEST_ACCOUNT_1_USERNAME").expect("Expected env config not found");
445 let password = "this is an invalid password for the test account";
446
447 let mut service = new_service_from_env();
448 let err = service.login(&username, &password).await.unwrap_err();
449 assert_eq!(io::ErrorKind::PermissionDenied, err.kind());
450 }
451
452 #[test]
453 #[ignore = "Requires environment configuration and is expensive"]
454 fn test_get_files() {
455 #[tokio::main]
456 async fn run(context: &mut TestContext) {
457 let username = context.do_login(1).await;
458 let mut service = context.service();
459
460 let mut needed_bytes = 0;
461 let mut needed_files = 0;
462 let mut filenames_and_contents = vec![];
463 for _ in 0..5 {
464 let (filename, content) = context.random_file();
465
466 needed_bytes += content.as_bytes().len() as u64;
467 needed_files += 1;
468 filenames_and_contents.push((filename, content));
469 }
470
471 let response = service.get_files(&username).await.unwrap();
472 for (filename, _content) in &filenames_and_contents {
473 assert!(!response.files.iter().any(|x| &x.filename == filename));
474 }
475 let disk_quota: DiskSpace = response.disk_quota.unwrap().into();
476 let disk_free: DiskSpace = response.disk_free.unwrap().into();
477 assert!(disk_quota.bytes() > 0);
478 assert!(disk_quota.files() > 0);
479 assert!(disk_free.bytes() >= needed_bytes, "Not enough space for test run");
480 assert!(disk_free.files() >= needed_files, "Not enough space for test run");
481
482 for (filename, _content) in &filenames_and_contents {
483 let request = GetFileRequest::default().with_get_content();
484 let err = service.get_file(&username, filename, &request).await.unwrap_err();
485 assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
486 }
487
488 for (filename, content) in &filenames_and_contents {
489 let request = PatchFileRequest::default().with_content(content.as_bytes());
490 service.patch_file(&username, filename, &request).await.unwrap();
491 }
492
493 let response = service.get_files(&username).await.unwrap();
494 for (filename, _content) in &filenames_and_contents {
495 assert!(response.files.iter().any(|x| &x.filename == filename));
496 }
497 }
498 run(&mut TestContext::new_from_env());
499 }
500
501 async fn do_get_and_patch_file_test(context: &mut TestContext, filename: &str, content: &[u8]) {
502 let username = context.do_login(1).await;
503 let mut service = context.service();
504
505 let request = PatchFileRequest::default().with_content(content);
506 service.patch_file(&username, filename, &request).await.unwrap();
507
508 let request = GetFileRequest::default().with_get_content();
509 let response = service.get_file(&username, filename, &request).await.unwrap();
510 assert_eq!(content, response.decoded_content().unwrap().unwrap());
511 }
512
513 #[test]
514 #[ignore = "Requires environment configuration and is expensive"]
515 fn test_get_and_patch_file_ok() {
516 #[tokio::main]
517 async fn run(context: &mut TestContext) {
518 let (filename, content) = context.random_file();
519 do_get_and_patch_file_test(context, &filename, content.as_bytes()).await;
520 }
521 run(&mut TestContext::new_from_env());
522 }
523
524 #[test]
525 #[ignore = "Requires environment configuration and is expensive"]
526 fn test_get_and_patch_file_empty_ok() {
527 #[tokio::main]
528 async fn run(context: &mut TestContext) {
529 let (filename, _content) = context.random_file();
530 do_get_and_patch_file_test(context, &filename, &[]).await;
531 }
532 run(&mut TestContext::new_from_env());
533 }
534
535 #[test]
536 #[ignore = "Requires environment configuration and is expensive"]
537 fn test_get_and_patch_file_utf8() {
538 #[tokio::main]
539 async fn run(context: &mut TestContext) {
540 let (filename, _content) = context.random_file();
541 let content = "안녕하세요";
542 do_get_and_patch_file_test(context, &filename, content.as_bytes()).await;
543 }
544 run(&mut TestContext::new_from_env());
545 }
546
547 #[test]
548 #[ignore = "Requires environment configuration and is expensive"]
549 fn test_get_file_not_found() {
550 #[tokio::main]
551 async fn run(context: &mut TestContext) {
552 let username = context.do_login(1).await;
553 let mut service = context.service();
554 let (filename, _content) = context.random_file();
555
556 let request = GetFileRequest::default().with_get_content();
557 let err = service.get_file(&username, &filename, &request).await.unwrap_err();
558 assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
559 }
560 run(&mut TestContext::new_from_env());
561 }
562
563 #[test]
564 #[ignore = "Requires environment configuration and is expensive"]
565 fn test_patch_file_without_login() {
566 #[tokio::main]
567 async fn run(context: &mut TestContext) {
568 let mut service = context.service();
569 let username = context.get_username(1);
570
571 context.do_login(1).await;
572 let (filename, _content) = context.random_file();
573
574 context.do_logout().await;
575 let request = PatchFileRequest::default().with_content("foo");
576 let err = service.patch_file(&username, &filename, &request).await.unwrap_err();
577 assert_eq!(io::ErrorKind::PermissionDenied, err.kind(), "{}", err);
578 assert!(format!("{}", err).contains("Not logged in"));
579 }
580 run(&mut TestContext::new_from_env());
581 }
582
583 #[test]
584 #[ignore = "Requires environment configuration and is expensive"]
585 fn test_acls_private() {
586 #[tokio::main]
587 async fn run(context: &mut TestContext) {
588 let mut service = context.service();
589
590 let (filename, content) = context.random_file();
591
592 let username1 = context.get_username(1);
593 let username2 = context.get_username(2);
594
595 let request = PatchFileRequest::default().with_content(content.clone());
597 context.do_login(1).await;
598 service.patch_file(&username1, &filename, &request).await.unwrap();
599
600 context.do_login(2).await;
602 let request = GetFileRequest::default().with_get_content();
603 let err = service.get_file(&username1, &filename, &request).await.unwrap_err();
604 assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
605
606 context.do_login(1).await;
608 let request = PatchFileRequest::default().with_add_readers([username2]);
609 service.patch_file(&username1, &filename, &request).await.unwrap();
610
611 context.do_login(2).await;
613 let request = GetFileRequest::default().with_get_content();
614 let response = service.get_file(&username1, &filename, &request).await.unwrap();
615 assert_eq!(content.as_bytes(), response.decoded_content().unwrap().unwrap());
616 }
617 run(&mut TestContext::new_from_env());
618 }
619
620 #[test]
621 #[ignore = "Requires environment configuration and is expensive"]
622 fn test_acls_public() {
623 #[tokio::main]
624 async fn run(context: &mut TestContext) {
625 let mut service = context.service();
626
627 let (filename, content) = context.random_file();
628
629 let username1 = context.get_username(1);
630
631 let request = PatchFileRequest::default().with_content(content.clone());
633 context.do_login(1).await;
634 service.patch_file(&username1, &filename, &request).await.unwrap();
635
636 context.do_logout().await;
638 let request = GetFileRequest::default().with_get_content();
639 let err = service.get_file(&username1, &filename, &request).await.unwrap_err();
640 assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
641
642 context.do_login(1).await;
644 let request = PatchFileRequest::default().with_add_readers(["public".to_owned()]);
645 service.patch_file(&username1, &filename, &request).await.unwrap();
646
647 context.do_logout().await;
649 let request = GetFileRequest::default().with_get_content();
650 let response = service.get_file(&username1, &filename, &request).await.unwrap();
651 assert_eq!(content.as_bytes(), response.decoded_content().unwrap().unwrap());
652 }
653 run(&mut TestContext::new_from_env());
654 }
655
656 #[test]
657 #[ignore = "Requires environment configuration and is expensive"]
658 fn test_delete_file_ok() {
659 #[tokio::main]
660 async fn run(context: &mut TestContext) {
661 let username = context.do_login(1).await;
662 let mut service = context.service();
663 let (filename, content) = context.random_file();
664
665 let request = PatchFileRequest::default().with_content(content);
666 service.patch_file(&username, &filename, &request).await.unwrap();
667
668 service.delete_file(&username, &filename).await.unwrap();
669
670 let request = GetFileRequest::default().with_get_content();
671 let err = service.get_file(&username, &filename, &request).await.unwrap_err();
672 assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
673 assert!(format!("{}", err).contains("(server code: 404"));
674 }
675 run(&mut TestContext::new_from_env());
676 }
677
678 #[test]
679 #[ignore = "Requires environment configuration and is expensive"]
680 fn test_delete_file_not_found() {
681 #[tokio::main]
682 async fn run(context: &mut TestContext) {
683 let username = context.do_login(1).await;
684 let mut service = context.service();
685 let (filename, _content) = context.random_file();
686
687 let err = service.delete_file(&username, &filename).await.unwrap_err();
688 assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
689 assert!(format!("{}", err).contains("(server code: 404"));
690 }
691 run(&mut TestContext::new_from_env());
692 }
693
694 #[test]
695 #[ignore = "Requires environment configuration and is expensive"]
696 fn test_delete_file_without_login() {
697 #[tokio::main]
698 async fn run(context: &mut TestContext) {
699 let mut service = context.service();
700 let username = context.get_username(1);
701
702 context.do_login(1).await;
703 let (filename, _content) = context.random_file();
704
705 context.do_logout().await;
706 let err = service.delete_file(&username, &filename).await.unwrap_err();
707 assert_eq!(io::ErrorKind::PermissionDenied, err.kind(), "{}", err);
708 assert!(format!("{}", err).contains("Not logged in"));
709 }
710 run(&mut TestContext::new_from_env());
711 }
712}