use std::{ cell::{RefCell, RefMut}, collections::{HashMap, hash_map::Entry}, }; use crate::{ Error, Result, error_response, file::{self, FileTrait}, sharry::{AliasID, Client, FileID, ShareID, Uri, json}, }; use super::mock_ids::CheckID; #[derive(Debug, Default)] pub struct MockClient { shares: RefCell>, } #[derive(Debug, Default)] struct MockShare { files: HashMap, } #[derive(Debug, Default)] struct MockFile { size: u64, offset: u64, } impl From<&file::Checked> for MockFile { fn from(value: &file::Checked) -> Self { Self { size: value.get_size(), offset: 0, } } } impl MockClient { fn insert_share(&self, share_id: &ShareID, share: MockShare) -> Result<()> { let mut shares = self.shares.borrow_mut(); let Entry::Vacant(entry) = shares.entry(share_id.to_string()) else { return Err(error_response!("can't insert share {share_id:?}!")); }; entry.insert(share); Ok(()) } fn insert_file(&self, share_id: &ShareID, file_id: &FileID, file: MockFile) -> Result<()> { let mut share = self.get_share(share_id)?; let Entry::Vacant(entry) = share.files.entry(file_id.to_string()) else { return Err(error_response!("can't insert file {file_id:?}!")); }; entry.insert(file); Ok(()) } fn get_share<'t>(&'t self, share_id: &ShareID) -> Result> { let share_id = &share_id.to_string(); let shares = self.shares.borrow_mut(); // check share exists shares .get(share_id) .ok_or_else(|| error_response!("can't find share {share_id:?}!"))?; Ok(RefMut::map(shares, |shares| { shares.get_mut(share_id).unwrap() })) } fn get_file<'t>( &'t self, share_id: &ShareID, file_id: &FileID, ) -> Result> { let file_id = &file_id.to_string(); let share = self.get_share(share_id)?; // check file exists share .files .get(file_id) .ok_or_else(|| error_response!("can't find file {file_id:?}!"))?; Ok(RefMut::map(share, move |share| { share.files.get_mut(file_id).unwrap() })) } } impl Client for MockClient { fn share_create( &self, uri: &Uri, alias_id: &AliasID, _: json::NewShareRequest, ) -> Result { (uri, alias_id).check()?; let share_id = true.into(); self.insert_share(&share_id, MockShare::default())?; Ok(share_id) } fn share_notify(&self, uri: &Uri, alias_id: &AliasID, share_id: &ShareID) -> crate::Result<()> { (uri, alias_id).check()?; share_id.check()?; let _share = self.get_share(share_id)?; Ok(()) } fn file_create( &self, uri: &Uri, alias_id: &AliasID, share_id: &ShareID, file: &file::Checked, ) -> Result { (uri, alias_id).check()?; share_id.check()?; let file_id = true.into(); self.insert_file(share_id, &file_id, file.into())?; Ok(file_id) } fn file_patch( &self, uri: &Uri, alias_id: &AliasID, share_id: &ShareID, chunk: &file::Chunk, ) -> Result<()> { (uri, alias_id).check()?; (share_id, chunk.get_file_id()).check()?; let file = self.get_file(share_id, chunk.get_file_id())?; if chunk.get_length() == 0 { return Err(error_response!("chunk {chunk:?} empty!")); } else if chunk.get_offset() % (1024 * 1024) != 0 { return Err(error_response!("chunk {chunk:?} not aligned to a MiB!")); } else if chunk.get_offset() != file.offset { return Error::mismatch(file.offset, chunk.get_offset()); } else if file.offset + chunk.get_length() > file.size { return Err(error_response!("chunk {chunk:?} too long!")); } let mut file = file; file.offset += chunk.get_length(); Ok(()) } } // technically redundant, but kept for refactoring purposes #[cfg(test)] mod tests { use std::collections::HashSet; use super::*; fn check_tostr_eq(left: L, right: R) where L: ExactSizeIterator, L::Item: ToString, R: ExactSizeIterator, R::Item: ToString, { assert_eq!(left.len(), right.len()); let l_strings: HashSet<_> = left.map(|s| s.to_string()).collect(); let r_strings: HashSet<_> = right.map(|s| s.to_string()).collect(); assert_eq!(l_strings, r_strings); } fn add_share(client: &MockClient) -> ShareID { let share_id = ShareID::from(true); client .insert_share(&share_id, MockShare::default()) .unwrap(); share_id } fn add_file(client: &MockClient, share_id: &ShareID) -> FileID { let file_id = FileID::from(true); client .insert_file(share_id, &file_id, MockFile::default()) .unwrap(); file_id } #[test] fn insert_share_works() { let client = MockClient::default(); let share_ids: [_; 10] = std::array::from_fn(|_| ShareID::from(true)); for share_id in share_ids.as_ref() { assert!(client.insert_share(share_id, MockShare::default()).is_ok()); } check_tostr_eq(client.shares.borrow().keys(), share_ids.iter()); } #[test] fn insert_share_double_errors() { let client = MockClient::default(); let share_ids: [_; 10] = std::array::from_fn(|_| add_share(&client)); for share_id in share_ids.as_ref() { let res = client.insert_share(&share_id, MockShare::default()); assert!(res.is_err()); assert!(res.unwrap_err().response_contains("can't insert share")); } } #[test] fn get_share_works() { let client = MockClient::default(); let share_ids: [_; 10] = std::array::from_fn(|_| add_share(&client)); for share_id in share_ids.as_ref() { assert!(client.get_share(share_id).is_ok()); } } #[test] fn get_share_nex_errors() { let client = MockClient::default(); add_share(&client); let share_ids_nex: [_; 10] = std::array::from_fn(|_| ShareID::from(true)); for share_id_nex in share_ids_nex.as_ref() { let res = client.get_share(share_id_nex); assert!(res.is_err()); assert!(res.unwrap_err().response_contains("can't find share")); } } #[test] fn insert_file_works() { let client = MockClient::default(); let share_id = add_share(&client); let file_ids: [_; 10] = std::array::from_fn(|_| FileID::from(true)); for file_id in file_ids.as_ref() { assert!( client .insert_file(&share_id, file_id, MockFile::default()) .is_ok() ); } let shares = client.shares.borrow(); let share = shares.get(&share_id.to_string()).unwrap(); check_tostr_eq(share.files.keys(), file_ids.iter()); } #[test] fn insert_file_nex_share_errors() { let client = MockClient::default(); add_share(&client); let share_id_nex = ShareID::default(); let res = client.insert_file(&share_id_nex, &FileID::from(true), MockFile::default()); assert!(res.is_err()); assert!(res.unwrap_err().response_contains("can't find share")); } #[test] fn insert_file_double_errors() { let client = MockClient::default(); let share_id = add_share(&client); let file_ids: [_; 10] = std::array::from_fn(|_| add_file(&client, &share_id)); for file_id in file_ids.as_ref() { let res = client.insert_file(&share_id, &file_id, MockFile::default()); assert!(res.is_err()); assert!(res.unwrap_err().response_contains("can't insert file")); } } #[test] fn get_file_works() { let client = MockClient::default(); let share_id = add_share(&client); let file_ids: [_; 10] = std::array::from_fn(|_| add_file(&client, &share_id)); for file_id in file_ids.as_ref() { assert!(client.get_file(&share_id, file_id).is_ok()); } } #[test] fn get_file_nex_errors() { let client = MockClient::default(); let share_id = add_share(&client); add_file(&client, &share_id); let file_ids_nex: [_; 10] = std::array::from_fn(|_| FileID::from(true)); for file_id_nex in file_ids_nex.as_ref() { let share_id_nex = ShareID::from(true); let res = client.get_file(&share_id_nex, file_id_nex); assert!(res.is_err()); assert!(res.unwrap_err().response_contains("can't find share")); let res = client.get_file(&share_id, file_id_nex); assert!(res.is_err()); assert!(res.unwrap_err().response_contains("can't find file")); } } }