Skip to main content

endbasic_client/
cloud.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Cloud-based implementation of the EndBASIC service client.
17
18use crate::*;
19use async_trait::async_trait;
20use base64::prelude::*;
21use bytes::Buf;
22use endbasic_std::console::remove_control_chars;
23use endbasic_std::storage::FileAcls;
24use reqwest::Response;
25use reqwest::StatusCode;
26use reqwest::header::HeaderMap;
27use std::cell::RefCell;
28use std::io;
29use std::rc::Rc;
30use std::str;
31use url::Url;
32
33/// Converts a `reqwest::Response` to an `io::Error`.  The response should have a non-OK status.
34async fn http_response_to_io_error(response: Response) -> io::Error {
35    let status = response.status();
36
37    let kind = match status {
38        StatusCode::OK => panic!("Should not have been called on a successful request"),
39
40        // Match against the codes we know the server explicitly hands us.
41        StatusCode::BAD_REQUEST => io::ErrorKind::InvalidInput,
42        StatusCode::FORBIDDEN => io::ErrorKind::PermissionDenied,
43        StatusCode::INSUFFICIENT_STORAGE => io::ErrorKind::Other,
44        StatusCode::INTERNAL_SERVER_ERROR => io::ErrorKind::Other,
45        StatusCode::NOT_FOUND => io::ErrorKind::NotFound,
46        StatusCode::PAYLOAD_TOO_LARGE => io::ErrorKind::InvalidInput,
47        StatusCode::SERVICE_UNAVAILABLE => io::ErrorKind::AddrNotAvailable,
48        StatusCode::UNAUTHORIZED => io::ErrorKind::PermissionDenied,
49
50        _ => io::ErrorKind::Other,
51    };
52
53    match response.text().await {
54        Ok(text) => match serde_json::from_str::<ErrorResponse>(&text) {
55            Ok(response) => io::Error::new(
56                kind,
57                format!("{} (server code: {})", remove_control_chars(response.message), status),
58            ),
59            _ => io::Error::new(
60                kind,
61                format!(
62                    "HTTP request returned status {} with text '{}'",
63                    status,
64                    remove_control_chars(text)
65                ),
66            ),
67        },
68        Err(e) => io::Error::new(
69            kind,
70            format!(
71                "HTTP request returned status {} and failed to get text due to {}",
72                status,
73                remove_control_chars(e.to_string())
74            ),
75        ),
76    }
77}
78
79/// Converts a `reqwest::Error` to an `io::Error`.
80fn reqwest_error_to_io_error(e: reqwest::Error) -> io::Error {
81    io::Error::other(format!("{}", e))
82}
83
84/// Container for authentication data to track after login.
85struct AuthData {
86    username: String,
87    access_token: AccessToken,
88}
89
90/// An implementation of the EndBASIC service client that talks to a remote server.
91pub struct CloudService {
92    api_address: Url,
93    client: reqwest::Client,
94    auth_data: Rc<RefCell<Option<AuthData>>>,
95}
96
97impl CloudService {
98    /// Creates a new client for the cloud service that talks to `api_address`.
99    pub fn new(api_address: &str) -> io::Result<Self> {
100        let url = match Url::parse(api_address) {
101            Ok(url) => url,
102            Err(e) => {
103                return Err(io::Error::new(
104                    io::ErrorKind::InvalidInput,
105                    format!("Invalid base API address: {}", e),
106                ));
107            }
108        };
109
110        if !(url.path().is_empty() || url.path() == "/") {
111            return Err(io::Error::new(
112                io::ErrorKind::InvalidInput,
113                "Invalid base API address: cannot contain a path".to_owned(),
114            ));
115        }
116
117        let auth_data = Rc::from(RefCell::from(None));
118
119        Ok(Self { api_address: url, client: reqwest::Client::default(), auth_data })
120    }
121
122    /// Generates a service URL with the given `path`.
123    fn make_url(&self, path: &str) -> Url {
124        assert!(path.starts_with("api/"));
125        let mut url = self.api_address.clone();
126        assert!(url.path().is_empty() || url.path() == "/");
127        url.set_path(path);
128        url
129    }
130
131    /// Returns the default headers to add to every request.
132    fn default_headers(&self) -> HeaderMap {
133        let mut headers = HeaderMap::new();
134        headers.insert(
135            "x-endbasic-client-version",
136            env!("CARGO_PKG_VERSION")
137                .parse()
138                .expect("Package version should have been serializable"),
139        );
140        headers
141    }
142
143    /// Checks if the given auth data object is present and returns it, or else returns a permission
144    /// denied error.
145    fn require_auth_data(data: Option<&AuthData>) -> io::Result<&AuthData> {
146        match data.as_ref() {
147            Some(data) => Ok(data),
148            None => {
149                Err(io::Error::new(io::ErrorKind::PermissionDenied, "Not logged in yet".to_owned()))
150            }
151        }
152    }
153}
154
155#[async_trait(?Send)]
156impl Service for CloudService {
157    async fn signup(&mut self, request: &SignupRequest) -> io::Result<()> {
158        let response = self
159            .client
160            .post(self.make_url("api/signup"))
161            .headers(self.default_headers())
162            .header("Content-Type", "application/json")
163            .body(serde_json::to_vec(&request)?)
164            .send()
165            .await
166            .map_err(reqwest_error_to_io_error)?;
167        match response.status() {
168            StatusCode::OK => Ok(()),
169            _ => Err(http_response_to_io_error(response).await),
170        }
171    }
172
173    async fn login(&mut self, username: &str, password: &str) -> io::Result<LoginResponse> {
174        // TODO(https://github.com/seanmonstar/reqwest/pull/1096): Replace with a basic_auth()
175        // call on the RequestBuilder once it is supported in WASM.
176        let basic_auth =
177            format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", username, password)));
178
179        let response = self
180            .client
181            .post(self.make_url("api/login"))
182            .headers(self.default_headers())
183            .header("Authorization", basic_auth)
184            .header("Content-Length", 0)
185            .send()
186            .await
187            .map_err(reqwest_error_to_io_error)?;
188        match response.status() {
189            StatusCode::OK => {
190                let bytes = response.bytes().await.map_err(reqwest_error_to_io_error)?;
191                let response: LoginResponse = serde_json::from_reader(bytes.reader())?;
192                let auth_data = AuthData {
193                    username: username.to_owned(),
194                    access_token: response.access_token.clone(),
195                };
196                *(self.auth_data.borrow_mut()) = Some(auth_data);
197                Ok(response)
198            }
199            _ => Err(http_response_to_io_error(response).await),
200        }
201    }
202
203    async fn logout(&mut self) -> io::Result<()> {
204        let mut auth_data = self.auth_data.borrow_mut();
205        let response = {
206            let auth_data = Self::require_auth_data(auth_data.as_ref())?;
207            self.client
208                .post(self.make_url(&format!("api/users/{}/logout", auth_data.username)))
209                .headers(self.default_headers())
210                .header("Content-Length", 0)
211                .bearer_auth(auth_data.access_token.as_str())
212                .send()
213                .await
214                .map_err(reqwest_error_to_io_error)?
215        };
216        match response.status() {
217            StatusCode::OK => {
218                *auth_data = None;
219                Ok(())
220            }
221            _ => Err(http_response_to_io_error(response).await),
222        }
223    }
224
225    fn is_logged_in(&self) -> bool {
226        self.auth_data.borrow().is_some()
227    }
228
229    fn logged_in_username(&self) -> Option<String> {
230        self.auth_data.borrow().as_ref().map(|x| x.username.to_owned())
231    }
232
233    async fn get_files(&mut self, username: &str) -> io::Result<GetFilesResponse> {
234        let mut builder = self
235            .client
236            .get(self.make_url(&format!("api/users/{}/files", username)))
237            .headers(self.default_headers());
238        if let Some(auth_data) = self.auth_data.borrow().as_ref() {
239            builder = builder.bearer_auth(auth_data.access_token.as_str());
240        }
241        let response = builder.send().await.map_err(reqwest_error_to_io_error)?;
242        match response.status() {
243            StatusCode::OK => {
244                let bytes = response.bytes().await.map_err(reqwest_error_to_io_error)?;
245                let response: GetFilesResponse = serde_json::from_reader(bytes.reader())?;
246                Ok(response)
247            }
248            _ => Err(http_response_to_io_error(response).await),
249        }
250    }
251
252    async fn get_file(&mut self, username: &str, filename: &str) -> io::Result<Vec<u8>> {
253        let mut builder = self
254            .client
255            .get(self.make_url(&format!("api/users/{}/files/{}", username, filename)))
256            .headers(self.default_headers());
257        if let Some(auth_data) = self.auth_data.borrow().as_ref() {
258            builder = builder.bearer_auth(auth_data.access_token.as_str());
259        }
260        let response = builder.send().await.map_err(reqwest_error_to_io_error)?;
261        match response.status() {
262            StatusCode::OK => {
263                Ok(response.bytes().await.map_err(reqwest_error_to_io_error)?.to_vec())
264            }
265            _ => Err(http_response_to_io_error(response).await),
266        }
267    }
268
269    async fn get_file_acls(&mut self, username: &str, filename: &str) -> io::Result<FileAcls> {
270        let mut headers = self.default_headers();
271        headers.insert("X-EndBASIC-GetContent", "false".parse().unwrap());
272        headers.insert("X-EndBASIC-GetReaders", "true".parse().unwrap());
273        let mut builder = self
274            .client
275            .get(self.make_url(&format!("api/users/{}/files/{}", username, filename)))
276            .headers(headers);
277        if let Some(auth_data) = self.auth_data.borrow().as_ref() {
278            builder = builder.bearer_auth(auth_data.access_token.as_str());
279        }
280        let response = builder.send().await.map_err(reqwest_error_to_io_error)?;
281        match response.status() {
282            StatusCode::OK => {
283                let mut readers = vec![];
284                for h in response.headers().get_all("X-EndBASIC-Reader") {
285                    match h.to_str() {
286                        Ok(value) => readers.push(value.to_owned()),
287                        Err(e) => {
288                            return Err(io::Error::new(
289                                io::ErrorKind::InvalidData,
290                                format!("Server returned invalid reader ACL: {}", e),
291                            ));
292                        }
293                    }
294                }
295
296                let bytes = response.bytes().await.map_err(reqwest_error_to_io_error)?;
297                debug_assert!(bytes.is_empty(), "Did not expect server to return content");
298
299                Ok(FileAcls::default().with_readers(readers))
300            }
301            _ => Err(http_response_to_io_error(response).await),
302        }
303    }
304
305    async fn patch_file_content(
306        &mut self,
307        username: &str,
308        filename: &str,
309        content: Vec<u8>,
310    ) -> io::Result<()> {
311        let auth_data = self.auth_data.borrow();
312
313        let response = self
314            .client
315            .patch(self.make_url(&format!("api/users/{}/files/{}", username, filename)))
316            .headers(self.default_headers())
317            .header("Content-Type", "application/octet-stream")
318            .header("X-EndBASIC-PatchContent", "true")
319            .body(content)
320            .bearer_auth(Self::require_auth_data(auth_data.as_ref())?.access_token.as_str())
321            .send()
322            .await
323            .map_err(reqwest_error_to_io_error)?;
324        match response.status() {
325            StatusCode::OK | StatusCode::CREATED => Ok(()),
326            _ => Err(http_response_to_io_error(response).await),
327        }
328    }
329
330    async fn patch_file_acls(
331        &mut self,
332        username: &str,
333        filename: &str,
334        add: &FileAcls,
335        remove: &FileAcls,
336    ) -> io::Result<()> {
337        let auth_data = self.auth_data.borrow();
338
339        let mut builder = self
340            .client
341            .patch(self.make_url(&format!("api/users/{}/files/{}", username, filename)))
342            .headers(self.default_headers())
343            .header("Content-Type", "application/octet-stream")
344            // Ensure we have at least one header to go through the header-based request handler.
345            .header("X-EndBASIC-PatchContent", "false");
346
347        for reader in add.readers() {
348            builder = builder.header("X-EndBASIC-AddReader", reader);
349        }
350        for reader in remove.readers() {
351            builder = builder.header("X-EndBASIC-RemoveReader", reader);
352        }
353
354        let response = builder
355            .bearer_auth(Self::require_auth_data(auth_data.as_ref())?.access_token.as_str())
356            .send()
357            .await
358            .map_err(reqwest_error_to_io_error)?;
359        match response.status() {
360            StatusCode::OK | StatusCode::CREATED => Ok(()),
361            _ => Err(http_response_to_io_error(response).await),
362        }
363    }
364
365    async fn delete_file(&mut self, username: &str, filename: &str) -> io::Result<()> {
366        let auth_data = self.auth_data.borrow();
367
368        let response = self
369            .client
370            .delete(self.make_url(&format!("api/users/{}/files/{}", username, filename)))
371            .headers(self.default_headers())
372            .header("Content-Length", 0)
373            .bearer_auth(Self::require_auth_data(auth_data.as_ref())?.access_token.as_str())
374            .send()
375            .await
376            .map_err(reqwest_error_to_io_error)?;
377        match response.status() {
378            StatusCode::OK => Ok(()),
379            _ => Err(http_response_to_io_error(response).await),
380        }
381    }
382}
383
384#[cfg(test)]
385mod testutils {
386    use super::*;
387    use std::collections::HashMap;
388    use std::env;
389
390    /// Wraps a `Service` to auto-delete created files.
391    pub struct AutoDeletingService<S: Service> {
392        /// The wrapped service.
393        service: S,
394
395        /// Current credentials a (username, password) pair.
396        current_user: Option<(String, String)>,
397
398        /// List of files to delete as a mapping of filename to the credentials required for deletion.
399        files_to_delete: HashMap<String, (String, String)>,
400    }
401
402    impl<S: Service> AutoDeletingService<S> {
403        /// Creates a new auto-deleting service that wraps `service`.
404        pub fn new(service: S) -> Self {
405            Self { service, current_user: None, files_to_delete: HashMap::default() }
406        }
407    }
408
409    #[async_trait(?Send)]
410    impl<S: Service> Service for AutoDeletingService<S> {
411        async fn signup(&mut self, request: &SignupRequest) -> io::Result<()> {
412            self.service.signup(request).await
413        }
414
415        async fn login(&mut self, username: &str, password: &str) -> io::Result<LoginResponse> {
416            let result = self.service.login(username, password).await;
417            if result.is_ok() {
418                self.current_user = Some((username.to_owned(), password.to_owned()));
419            }
420            result
421        }
422
423        async fn logout(&mut self) -> io::Result<()> {
424            let result = self.service.logout().await;
425            if result.is_ok() {
426                self.current_user = None;
427            }
428            result
429        }
430
431        fn is_logged_in(&self) -> bool {
432            self.service.is_logged_in()
433        }
434
435        fn logged_in_username(&self) -> Option<String> {
436            self.service.logged_in_username()
437        }
438
439        async fn get_files(&mut self, username: &str) -> io::Result<GetFilesResponse> {
440            self.service.get_files(username).await
441        }
442
443        async fn get_file(&mut self, username: &str, filename: &str) -> io::Result<Vec<u8>> {
444            self.service.get_file(username, filename).await
445        }
446
447        async fn get_file_acls(&mut self, username: &str, filename: &str) -> io::Result<FileAcls> {
448            self.service.get_file_acls(username, filename).await
449        }
450
451        async fn patch_file_content(
452            &mut self,
453            username: &str,
454            filename: &str,
455            content: Vec<u8>,
456        ) -> io::Result<()> {
457            let result = self.service.patch_file_content(username, filename, content).await;
458            if result.is_ok() {
459                self.files_to_delete
460                    .insert(filename.to_owned(), self.current_user.clone().unwrap());
461            }
462            result
463        }
464
465        async fn patch_file_acls(
466            &mut self,
467            username: &str,
468            filename: &str,
469            add: &FileAcls,
470            remove: &FileAcls,
471        ) -> io::Result<()> {
472            self.service.patch_file_acls(username, filename, add, remove).await
473        }
474
475        async fn delete_file(&mut self, username: &str, filename: &str) -> io::Result<()> {
476            let result = self.service.delete_file(username, filename).await;
477            if result.is_ok() {
478                self.files_to_delete.remove(filename);
479            }
480            result
481        }
482    }
483
484    impl<S: Service> Drop for AutoDeletingService<S> {
485        fn drop(&mut self) {
486            #[tokio::main]
487            #[allow(clippy::single_match)]
488            async fn cleanup<S: Service>(service: &mut AutoDeletingService<S>) {
489                if let Some((username, _password)) = service.current_user.as_ref() {
490                    service
491                        .service
492                        .logout()
493                        .await
494                        .map_err(|e| {
495                            format!("Failed to log out for {} during cleanup: {}", username, e)
496                        })
497                        .unwrap();
498                }
499
500                for (filename, (username, password)) in service.files_to_delete.iter() {
501                    service
502                        .service
503                        .login(username, password)
504                        .await
505                        .map_err(|e| {
506                            format!("Failed to log in for {} during cleanup: {}", username, e)
507                        })
508                        .unwrap();
509
510                    service
511                        .service
512                        .delete_file(username, filename)
513                        .await
514                        .map_err(|e| {
515                            format!("Failed to delete file {} during cleanup: {}", filename, e)
516                        })
517                        .unwrap();
518
519                    service
520                        .service
521                        .logout()
522                        .await
523                        .map_err(|e| {
524                            format!("Failed to log out for {} during cleanup: {}", username, e)
525                        })
526                        .unwrap();
527                }
528            }
529            cleanup(self);
530        }
531    }
532
533    /// Creates a new service that talks to the configured API service for testing.
534    pub(crate) fn new_service_from_env() -> AutoDeletingService<CloudService> {
535        let service_api = env::var("SERVICE_URL").expect("Expected env config not found");
536        AutoDeletingService::new(CloudService::new(&service_api).unwrap())
537    }
538
539    /// Holds state for a test and allows for automatic cleanup of shared resources.
540    pub(crate) struct TestContext {
541        pub(super) service: AutoDeletingService<CloudService>,
542    }
543
544    impl TestContext {
545        /// Creates a new test context that talks to the configured API service.
546        pub(crate) fn new_from_env() -> Self {
547            TestContext { service: new_service_from_env() }
548        }
549
550        /// Returns the username of the selected test account.
551        pub(crate) fn get_username(&self, i: u8) -> String {
552            env::var(format!("TEST_ACCOUNT_{}_USERNAME", i)).expect("Expected env config not found")
553        }
554
555        /// Authenticates against the server using the username and password passed in via
556        /// environment variables.  We need to support multiple test accounts at the same time, so
557        /// this performs authentication for the test account `i`.
558        ///
559        /// Returns the username of the selected test account for convenience.
560        pub(crate) async fn do_login(&mut self, i: u8) -> String {
561            let username = self.get_username(i);
562            let password = env::var(format!("TEST_ACCOUNT_{}_PASSWORD", i))
563                .expect("Expected env config not found");
564            let _response = self.service.login(&username, &password).await.unwrap();
565            username
566        }
567
568        /// Clears the authentication token to represent a log out.
569        pub(crate) async fn do_logout(&mut self) {
570            self.service.logout().await.unwrap();
571        }
572
573        /// Generates a random filename and its content for testing, and makes sure the file gets
574        /// deleted during cleanup in case the test didn't do it on its own.
575        pub(crate) fn random_file(&mut self) -> (String, Vec<u8>) {
576            let filename = format!("file-{}", rand::random::<u64>());
577            let content = format!("Test content for {}", filename);
578            (filename, content.into_bytes())
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    //! Tests against the real EndBASIC service.
586    //!
587    //! These tests are configured with the username/password of a test account that we know exists,
588    //! but that limits the kind of testing we can do (especially around authentication/login).
589    //! We could instead mock out the HTTP client to inject arbitrary responses in our tests, which
590    //! would allow us to validate JSON deserialization and the like... but then the tests would be
591    //! so unrealistic as to not be useful.
592
593    use super::testutils::*;
594    use super::*;
595    use std::env;
596
597    #[test]
598    #[ignore = "Requires environment configuration and is expensive"]
599    fn test_login_ok() {
600        #[tokio::main]
601        async fn run(context: &mut TestContext) {
602            let _username = context.do_login(1).await;
603        }
604        run(&mut TestContext::new_from_env());
605    }
606
607    #[test]
608    #[ignore = "Requires environment configuration and is expensive"]
609    fn test_login_bad_password() {
610        #[tokio::main]
611        async fn run(context: &mut TestContext) {
612            let username =
613                env::var("TEST_ACCOUNT_1_USERNAME").expect("Expected env config not found");
614            let password = "this is an invalid password for the test account";
615
616            let err = context.service.login(&username, password).await.unwrap_err();
617            assert_eq!(io::ErrorKind::PermissionDenied, err.kind());
618        }
619        run(&mut TestContext::new_from_env());
620    }
621
622    #[test]
623    #[ignore = "Requires environment configuration and is expensive"]
624    fn test_get_files() {
625        #[tokio::main]
626        async fn run(context: &mut TestContext) {
627            let username = context.do_login(1).await;
628
629            let mut needed_bytes = 0;
630            let mut needed_files = 0;
631            let mut filenames_and_contents = vec![];
632            for _ in 0..5 {
633                let (filename, content) = context.random_file();
634
635                needed_bytes += content.len() as u64;
636                needed_files += 1;
637                filenames_and_contents.push((filename, content));
638            }
639
640            let response = context.service.get_files(&username).await.unwrap();
641            for (filename, _content) in &filenames_and_contents {
642                assert!(!response.files.iter().any(|x| &x.filename == filename));
643            }
644            let disk_quota: DiskSpace = response.disk_quota.unwrap().into();
645            let disk_free: DiskSpace = response.disk_free.unwrap().into();
646            assert!(disk_quota.bytes() > 0);
647            assert!(disk_quota.files() > 0);
648            assert!(disk_free.bytes() >= needed_bytes, "Not enough space for test run");
649            assert!(disk_free.files() >= needed_files, "Not enough space for test run");
650
651            for (filename, _content) in &filenames_and_contents {
652                let err = context.service.get_file(&username, filename).await.unwrap_err();
653                assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
654            }
655
656            for (filename, content) in &filenames_and_contents {
657                context
658                    .service
659                    .patch_file_content(&username, filename, content.clone())
660                    .await
661                    .unwrap();
662            }
663
664            let response = context.service.get_files(&username).await.unwrap();
665            for (filename, _content) in &filenames_and_contents {
666                assert!(response.files.iter().any(|x| &x.filename == filename));
667            }
668        }
669        run(&mut TestContext::new_from_env());
670    }
671
672    async fn do_get_and_patch_file_test<B: Into<Vec<u8>>>(
673        context: &mut TestContext,
674        filename: &str,
675        content: B,
676    ) {
677        let username = context.do_login(1).await;
678
679        let content = content.into();
680        context.service.patch_file_content(&username, filename, content.clone()).await.unwrap();
681        assert_eq!(content, context.service.get_file(&username, filename).await.unwrap());
682    }
683
684    #[test]
685    #[ignore = "Requires environment configuration and is expensive"]
686    fn test_get_and_patch_file_ok() {
687        #[tokio::main]
688        async fn run(context: &mut TestContext) {
689            let (filename, content) = context.random_file();
690            do_get_and_patch_file_test(context, &filename, content).await;
691        }
692        run(&mut TestContext::new_from_env());
693    }
694
695    #[test]
696    #[ignore = "Requires environment configuration and is expensive"]
697    fn test_get_and_patch_file_empty_ok() {
698        #[tokio::main]
699        async fn run(context: &mut TestContext) {
700            let (filename, _content) = context.random_file();
701            do_get_and_patch_file_test(context, &filename, &[]).await;
702        }
703        run(&mut TestContext::new_from_env());
704    }
705
706    #[test]
707    #[ignore = "Requires environment configuration and is expensive"]
708    fn test_get_and_patch_file_utf8() {
709        #[tokio::main]
710        async fn run(context: &mut TestContext) {
711            let (filename, _content) = context.random_file();
712            let content = "안녕하세요";
713            do_get_and_patch_file_test(context, &filename, content).await;
714        }
715        run(&mut TestContext::new_from_env());
716    }
717
718    #[test]
719    #[ignore = "Requires environment configuration and is expensive"]
720    fn test_get_file_not_found() {
721        #[tokio::main]
722        async fn run(context: &mut TestContext) {
723            let username = context.do_login(1).await;
724            let (filename, _content) = context.random_file();
725
726            let err = context.service.get_file(&username, &filename).await.unwrap_err();
727            assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
728        }
729        run(&mut TestContext::new_from_env());
730    }
731
732    #[test]
733    #[ignore = "Requires environment configuration and is expensive"]
734    fn test_patch_file_without_login() {
735        #[tokio::main]
736        async fn run(context: &mut TestContext) {
737            let username = context.get_username(1);
738
739            context.do_login(1).await;
740            let (filename, _content) = context.random_file();
741
742            context.do_logout().await;
743            let err = context
744                .service
745                .patch_file_content(&username, &filename, b"foo".to_vec())
746                .await
747                .unwrap_err();
748            assert_eq!(io::ErrorKind::PermissionDenied, err.kind(), "{}", err);
749            assert!(format!("{}", err).contains("Not logged in"));
750        }
751        run(&mut TestContext::new_from_env());
752    }
753
754    #[test]
755    #[ignore = "Requires environment configuration and is expensive"]
756    fn test_acls_private() {
757        #[tokio::main]
758        async fn run(context: &mut TestContext) {
759            let (filename, content) = context.random_file();
760
761            let username1 = context.get_username(1);
762            let username2 = context.get_username(2);
763
764            // Share username1's file with username2.
765            context.do_login(1).await;
766            context
767                .service
768                .patch_file_content(&username1, &filename, content.clone())
769                .await
770                .unwrap();
771
772            // Read username1's file as username2 before it is shared.
773            context.do_login(2).await;
774            let err = context.service.get_file(&username1, &filename).await.unwrap_err();
775            assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
776
777            // Share username1's file with username2.
778            context.do_login(1).await;
779            context
780                .service
781                .patch_file_acls(
782                    &username1,
783                    &filename,
784                    &FileAcls::default().with_readers([username2]),
785                    &FileAcls::default(),
786                )
787                .await
788                .unwrap();
789
790            // Read username1's file as username2 again, now that it is shared.
791            context.do_login(2).await;
792            let response = context.service.get_file(&username1, &filename).await.unwrap();
793            assert_eq!(content, response);
794        }
795        run(&mut TestContext::new_from_env());
796    }
797
798    #[test]
799    #[ignore = "Requires environment configuration and is expensive"]
800    fn test_acls_public() {
801        #[tokio::main]
802        async fn run(context: &mut TestContext) {
803            let (filename, content) = context.random_file();
804
805            let username1 = context.get_username(1);
806
807            // Share username1's file with the public.
808            context.do_login(1).await;
809            context
810                .service
811                .patch_file_content(&username1, &filename, content.clone())
812                .await
813                .unwrap();
814
815            // Read username1's file as a guest before it is shared.
816            context.do_logout().await;
817            let err = context.service.get_file(&username1, &filename).await.unwrap_err();
818            assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
819
820            // Share username1's file with the public.
821            context.do_login(1).await;
822            context
823                .service
824                .patch_file_acls(
825                    &username1,
826                    &filename,
827                    &FileAcls::default().with_readers(["public".to_owned()]),
828                    &FileAcls::default(),
829                )
830                .await
831                .unwrap();
832
833            // Read username1's file as a guest again, now that it is shared.
834            context.do_logout().await;
835            let response = context.service.get_file(&username1, &filename).await.unwrap();
836            assert_eq!(content, response);
837        }
838        run(&mut TestContext::new_from_env());
839    }
840
841    #[test]
842    #[ignore = "Requires environment configuration and is expensive"]
843    fn test_delete_file_ok() {
844        #[tokio::main]
845        async fn run(context: &mut TestContext) {
846            let username = context.do_login(1).await;
847            let (filename, content) = context.random_file();
848
849            context.service.patch_file_content(&username, &filename, content).await.unwrap();
850
851            context.service.delete_file(&username, &filename).await.unwrap();
852
853            let err = context.service.get_file(&username, &filename).await.unwrap_err();
854            assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
855            assert!(format!("{}", err).contains("(server code: 404"));
856        }
857        run(&mut TestContext::new_from_env());
858    }
859
860    #[test]
861    #[ignore = "Requires environment configuration and is expensive"]
862    fn test_delete_file_not_found() {
863        #[tokio::main]
864        async fn run(context: &mut TestContext) {
865            let username = context.do_login(1).await;
866            let (filename, _content) = context.random_file();
867
868            let err = context.service.delete_file(&username, &filename).await.unwrap_err();
869            assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err);
870            assert!(format!("{}", err).contains("(server code: 404"));
871        }
872        run(&mut TestContext::new_from_env());
873    }
874
875    #[test]
876    #[ignore = "Requires environment configuration and is expensive"]
877    fn test_delete_file_without_login() {
878        #[tokio::main]
879        async fn run(context: &mut TestContext) {
880            let username = context.get_username(1);
881
882            context.do_login(1).await;
883            let (filename, _content) = context.random_file();
884
885            context.do_logout().await;
886            let err = context.service.delete_file(&username, &filename).await.unwrap_err();
887            assert_eq!(io::ErrorKind::PermissionDenied, err.kind(), "{}", err);
888            assert!(format!("{}", err).contains("Not logged in"));
889        }
890        run(&mut TestContext::new_from_env());
891    }
892}