Compare commits

..

3 commits

Author SHA1 Message Date
b3bccbbf65 [wip] unit testing
- minor refactoring
2025-07-09 15:57:47 +00:00
d4cc102a0f [wip] unit tests for file module
- test for `Checked::start_upload`
2025-07-09 15:57:43 +00:00
3257a97351 [wip] unit testing
- mock impl for `sharry::Client` and associated IDs
2025-07-08 21:41:38 +00:00
8 changed files with 176 additions and 82 deletions

35
Cargo.lock generated
View file

@ -79,6 +79,15 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "atomic"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@ -108,6 +117,12 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytemuck"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.10.1" version = "1.10.1"
@ -874,6 +889,12 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.20"
@ -944,6 +965,7 @@ dependencies = [
"tempfile", "tempfile",
"thiserror 2.0.12", "thiserror 2.0.12",
"ureq", "ureq",
"uuid",
] ]
[[package]] [[package]]
@ -1166,6 +1188,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"atomic",
"getrandom 0.3.3",
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@ -1195,6 +1229,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
"rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]

View file

@ -23,6 +23,7 @@ ureq = { version = "3.0.11", features = ["json"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.20.0" tempfile = "3.20.0"
uuid = { version = "1.17.0", features = ["rng", "std", "v1"] }
[profile.release] [profile.release]
# Optimize for speed even more aggressively # Optimize for speed even more aggressively

View file

@ -154,3 +154,13 @@ impl Error {
} }
} }
} }
#[macro_export]
macro_rules! error_response {
// Match a format string plus optional arguments
($fmt:expr $(, $arg:expr )* $(,)?) => {
// Expand to constructing the Error::Response variant,
// wrapping a `format!(...)` call
Error::Response(format!($fmt $(, $arg )*))
};
}

View file

@ -115,12 +115,10 @@ impl FileTrait for Checked {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// tests for `Checked::start_upload` omitted, as they require a `sharry::Client`
use tempfile::TempDir; use tempfile::TempDir;
use crate::test_util::{ use crate::test_util::{
create_file, MockClient, create_file,
data::{HASHES_STD_GOOD, cases, data}, data::{HASHES_STD_GOOD, cases, data},
}; };
@ -204,4 +202,25 @@ mod tests {
assert!(err.is_mismatch("unhashed file", chk.path.display().to_string())); assert!(err.is_mismatch("unhashed file", chk.path.display().to_string()));
} }
} }
#[test]
fn start_upload_works() {
let client = MockClient::default();
let share_id = client.add_share();
for content in data() {
let file = create_file(content);
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
assert!(
chk.start_upload(
&client,
&sharry::Uri::from(true),
&sharry::AliasID::from(true),
&share_id
)
.is_ok()
);
}
}
} }

View file

@ -76,7 +76,10 @@ mod tests {
); );
check_trait(format!("{chunk:?}"), repr_expect, "Debug", "Chunk"); check_trait(format!("{chunk:?}"), repr_expect, "Debug", "Chunk");
assert_eq!(chunk.get_file_id().to_string(), ""); assert_eq!(
chunk.get_file_id().to_string(),
sharry::FileID::default().to_string()
);
assert_eq!(chunk.get_offset(), mock_offset); assert_eq!(chunk.get_offset(), mock_offset);
assert_eq!(chunk.get_data(), data); assert_eq!(chunk.get_data(), data);
assert_eq!(chunk.get_length(), len); assert_eq!(chunk.get_length(), len);

View file

@ -4,14 +4,14 @@ use std::{
}; };
use crate::{ use crate::{
Error, Result, Error, Result, error_response,
file::{self, FileTrait}, file::{self, FileTrait},
sharry::{AliasID, Client, FileID, ShareID, Uri, json}, sharry::{AliasID, Client, FileID, ShareID, Uri, json},
}; };
use super::mock_ids::CheckID; use super::mock_ids::CheckID;
#[derive(Debug)] #[derive(Debug, Default)]
pub struct MockClient { pub struct MockClient {
shares: RefCell<HashMap<String, MockShare>>, shares: RefCell<HashMap<String, MockShare>>,
} }
@ -23,82 +23,84 @@ struct MockShare {
#[derive(Debug)] #[derive(Debug)]
struct MockFile { struct MockFile {
name: String,
size: u64, size: u64,
offset: u64, offset: u64,
} }
impl From<&file::Checked> for MockFile {
fn from(value: &file::Checked) -> Self {
Self {
size: value.get_size(),
offset: 0,
}
}
}
impl MockClient { impl MockClient {
fn insert_share(&self, share_id: &ShareID) -> Result<()> { fn insert_share(&self, share_id: &ShareID, share: MockShare) -> Result<()> {
let mut shares = self.shares.borrow_mut(); let mut shares = self.shares.borrow_mut();
let Entry::Vacant(entry) = shares.entry(share_id.to_string()) else { let Entry::Vacant(entry) = shares.entry(share_id.to_string()) else {
return Err(Error::response(format_args!( return Err(error_response!("can't insert share {share_id:?}!"));
"Can't create share {share_id:?}!"
)));
}; };
entry.insert(MockShare::default()); entry.insert(share);
Ok(()) Ok(())
} }
fn insert_file( fn insert_file(&self, share_id: &ShareID, file_id: &FileID, file: MockFile) -> Result<()> {
&self, let mut share = self.get_share(share_id)?;
share_id: &ShareID,
file_id: &FileID,
name: String,
size: u64,
) -> Result<()> {
let mut share = self.get_share_mut(share_id)?;
let Entry::Vacant(entry) = share.files.entry(file_id.to_string()) else { let Entry::Vacant(entry) = share.files.entry(file_id.to_string()) else {
return Err(Error::response(format_args!( return Err(error_response!("can't insert file {file_id:?}!"));
"Can't create file {file_id:?}!"
)));
}; };
entry.insert(MockFile { entry.insert(file);
name,
size,
offset: 0,
});
Ok(()) Ok(())
} }
fn get_share_mut<'t>(&'t self, share_id: &ShareID) -> Result<RefMut<'t, MockShare>> { fn get_share<'t>(&'t self, share_id: &ShareID) -> Result<RefMut<'t, MockShare>> {
let share_id = &share_id.to_string(); let share_id = &share_id.to_string();
let shares = self.shares.borrow_mut(); let shares = self.shares.borrow_mut();
// check share exists
shares shares
.get(share_id) .get(share_id)
.ok_or_else(|| Error::response(format_args!("Can't find share {share_id:?}!")))?; .ok_or_else(|| error_response!("can't find share {share_id:?}!"))?;
// share exists
Ok(RefMut::map(shares, |shares| { Ok(RefMut::map(shares, |shares| {
shares.get_mut(share_id).expect("checked but None!") shares.get_mut(share_id).expect("checked but None!")
})) }))
} }
fn get_file_mut<'t>( fn get_file<'t>(
&'t self, &'t self,
share_id: &ShareID, share_id: &ShareID,
file_id: &FileID, file_id: &FileID,
) -> Result<RefMut<'t, MockFile>> { ) -> Result<RefMut<'t, MockFile>> {
let file_id = &file_id.to_string(); let file_id = &file_id.to_string();
let share = self.get_share_mut(share_id)?; let share = self.get_share(share_id)?;
// check file exists
share share
.files .files
.get(file_id) .get(file_id)
.ok_or_else(|| Error::response(format_args!("Can't find file {file_id:?}!")))?; .ok_or_else(|| error_response!("can't find file {file_id:?}!"))?;
// file exists
Ok(RefMut::map(share, move |share| { Ok(RefMut::map(share, move |share| {
share.files.get_mut(file_id).expect("checked but None!") share.files.get_mut(file_id).expect("checked but None!")
})) }))
} }
pub fn add_share(&self) -> ShareID {
let share_id = ShareID::from(true);
self.insert_share(&share_id, MockShare::default())
.expect("should never fail");
share_id
}
} }
impl Client for MockClient { impl Client for MockClient {
@ -111,7 +113,7 @@ impl Client for MockClient {
(uri, alias_id).check()?; (uri, alias_id).check()?;
let share_id = true.into(); let share_id = true.into();
self.insert_share(&share_id)?; self.insert_share(&share_id, MockShare::default())?;
Ok(share_id) Ok(share_id)
} }
@ -120,6 +122,8 @@ impl Client for MockClient {
(uri, alias_id).check()?; (uri, alias_id).check()?;
share_id.check()?; share_id.check()?;
let _share = self.get_share(share_id)?;
Ok(()) Ok(())
} }
@ -134,12 +138,7 @@ impl Client for MockClient {
share_id.check()?; share_id.check()?;
let file_id = true.into(); let file_id = true.into();
self.insert_file( self.insert_file(share_id, &file_id, file.into())?;
share_id,
&file_id,
file.get_name().to_string(),
file.get_size(),
)?;
Ok(file_id) Ok(file_id)
} }
@ -154,11 +153,21 @@ impl Client for MockClient {
(uri, alias_id).check()?; (uri, alias_id).check()?;
(share_id, chunk.get_file_id()).check()?; (share_id, chunk.get_file_id()).check()?;
// TODO: `chunk` must align to a full MiB let file = self.get_file(share_id, chunk.get_file_id())?;
let file = self.get_file_mut(share_id, chunk.get_file_id())?;
todo!() 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!"));
}
// Ok(()) let mut file = file;
file.offset += chunk.get_length();
Ok(())
} }
} }

View file

@ -1,3 +1,5 @@
use uuid::Uuid;
use crate::{ use crate::{
Result, Result,
sharry::{AliasID, FileID, ShareID, Uri}, sharry::{AliasID, FileID, ShareID, Uri},
@ -10,11 +12,25 @@ const VALID_FILE: &str = "valid-file";
fn make_invalid(valid: &str) -> String { fn make_invalid(valid: &str) -> String {
let invalid = valid.replace("valid", "invalid"); let invalid = valid.replace("valid", "invalid");
assert_ne!(valid, invalid);
assert_ne!(invalid, valid);
invalid invalid
} }
fn make_valid(valid: &str) -> String {
let invalid = make_invalid(valid);
let valid = {
let id = Uuid::now_v1(&[4, 8, 15, 16, 23, 42]);
valid.replace("valid", &id.to_string())
};
assert_ne!(valid, invalid);
valid
}
pub trait CheckID { pub trait CheckID {
fn check(self) -> Result<()>; fn check(self) -> Result<()>;
} }
@ -56,7 +72,7 @@ impl CheckID for (&ShareID, &FileID) {
impl From<bool> for Uri { impl From<bool> for Uri {
fn from(value: bool) -> Self { fn from(value: bool) -> Self {
if value { if value {
Self::from(VALID_URI.to_string()) Self::from(make_valid(VALID_URI))
} else { } else {
Self::from(make_invalid(VALID_URI)) Self::from(make_invalid(VALID_URI))
} }
@ -66,7 +82,7 @@ impl From<bool> for Uri {
impl From<bool> for AliasID { impl From<bool> for AliasID {
fn from(value: bool) -> Self { fn from(value: bool) -> Self {
if value { if value {
Self::from(VALID_ALIAS.to_string()) Self::from(make_valid(VALID_ALIAS))
} else { } else {
Self::from(make_invalid(VALID_ALIAS)) Self::from(make_invalid(VALID_ALIAS))
} }
@ -76,7 +92,7 @@ impl From<bool> for AliasID {
impl From<bool> for ShareID { impl From<bool> for ShareID {
fn from(value: bool) -> Self { fn from(value: bool) -> Self {
if value { if value {
Self::from(VALID_SHARE.to_string()) Self::from(make_valid(VALID_SHARE))
} else { } else {
Self::from(make_invalid(VALID_SHARE)) Self::from(make_invalid(VALID_SHARE))
} }
@ -86,7 +102,7 @@ impl From<bool> for ShareID {
impl From<bool> for FileID { impl From<bool> for FileID {
fn from(value: bool) -> Self { fn from(value: bool) -> Self {
if value { if value {
Self::new_test(VALID_FILE) Self::new_test(make_valid(VALID_FILE))
} else { } else {
Self::new_test(make_invalid(VALID_FILE)) Self::new_test(make_invalid(VALID_FILE))
} }
@ -106,61 +122,60 @@ mod tests {
let share_id = ShareID::from(true); let share_id = ShareID::from(true);
let file_id = FileID::from(true); let file_id = FileID::from(true);
assert!(matches!((&uri, &alias_id).check(), Ok(()))); assert!((&uri, &alias_id).check().is_ok());
assert!(matches!(share_id.check(), Ok(()))); assert!(share_id.check().is_ok());
assert!(matches!((&share_id, &file_id).check(), Ok(()))); assert!((&share_id, &file_id).check().is_ok());
} }
#[test] #[test]
fn default_is_valid() { fn default_is_valid() {
let uri = Uri::default(); let uri = Uri::default();
let alias_id = AliasID::from(true); // no `impl Default`
let share_id = ShareID::default(); let share_id = ShareID::default();
let file_id = FileID::default(); let file_id = FileID::default();
assert!(matches!((&uri, &AliasID::from(true)).check(), Ok(()))); assert!((&uri, &alias_id).check().is_ok());
assert!(matches!(share_id.check(), Ok(()))); assert!(share_id.check().is_ok());
assert!(matches!((&share_id, &file_id).check(), Ok(()))); assert!((&share_id, &file_id).check().is_ok());
} }
#[test] #[test]
fn false_makes_invalids() { fn false_makes_invalids() {
fn test_check(value: impl CheckID, callback: impl FnOnce(&Parameter) -> bool) {
let check = value.check().expect_err("should be invalid");
let p = check.get_invalid_param().expect("should be InvalidParam");
assert!(callback(p));
}
// valid ids
let uri = Uri::from(true); let uri = Uri::from(true);
let alias_id = AliasID::from(true); let alias_id = AliasID::from(true);
let share_id = ShareID::from(true); let share_id = ShareID::from(true);
let file_id = FileID::from(true); let file_id = FileID::from(true);
// invalid ids
let uri_i = Uri::from(false); let uri_i = Uri::from(false);
let alias_id_i = AliasID::from(false); let alias_id_i = AliasID::from(false);
let share_id_i = ShareID::from(false); let share_id_i = ShareID::from(false);
let file_id_i = FileID::from(false); let file_id_i = FileID::from(false);
// invalid Uri, valid alias // param checks
let check = (&uri_i, &alias_id).check().expect_err("should be invalid"); let is_uri_i = |p: &Parameter| matches!(p, Parameter::Uri(_));
let p = check.get_invalid_param().expect("should be InvalidParam"); let is_alias_id_i = |p: &Parameter| matches!(p, Parameter::AliasID(_));
assert!(matches!(p, Parameter::Uri(_))); let is_share_id_i = |p: &Parameter| matches!(p, Parameter::ShareID(_));
let is_file_id_i = |p: &Parameter| matches!(p, Parameter::FileID(_));
// valid Uri, invalid alias // uri + alias
let check = (&uri, &alias_id_i).check().expect_err("should be invalid"); test_check((&uri_i, &alias_id_i), is_uri_i);
let p = check.get_invalid_param().expect("should be InvalidParam"); test_check((&uri_i, &alias_id), is_uri_i);
assert!(matches!(p, Parameter::AliasID(_))); test_check((&uri, &alias_id_i), is_alias_id_i);
// invalid share // share
let check = share_id_i.check().expect_err("should be invalid"); test_check(&share_id_i, is_share_id_i);
let p = check.get_invalid_param().expect("should be InvalidParam");
assert!(matches!(p, Parameter::ShareID(_)));
// invalid share, valid file // share + file
let check = (&share_id_i, &file_id) test_check((&share_id_i, &file_id_i), is_share_id_i);
.check() test_check((&share_id_i, &file_id), is_share_id_i);
.expect_err("should be invalid"); test_check((&share_id, &file_id_i), is_file_id_i);
let p = check.get_invalid_param().expect("should be InvalidParam");
assert!(matches!(p, Parameter::ShareID(_)));
// valid share, invalid file
let check = (&share_id, &file_id_i)
.check()
.expect_err("should be invalid");
let p = check.get_invalid_param().expect("should be InvalidParam");
assert!(matches!(p, Parameter::FileID(_)));
} }
} }

View file

@ -4,6 +4,8 @@ pub mod data;
mod mock_client; mod mock_client;
mod mock_ids; mod mock_ids;
pub use mock_client::MockClient;
use std::{fmt, io::Write}; use std::{fmt, io::Write};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;