Compare commits

..

8 commits

Author SHA1 Message Date
61d62d731e [wip] unit tests for file module
- testing for `compute_hash` and `check_hash`
2025-07-03 17:20:56 +00:00
6814f74484 base64 usage 2025-07-03 16:20:39 +00:00
6c385ffeea [wip] unit tests for file module
- testing for `compute_hash` and `check_hash`
2025-07-03 16:18:46 +00:00
cab6d13d28 test data in arrays, not vec! 2025-07-03 15:43:26 +00:00
cca35e1ae8 [wip] unit tests for file module
- testing for `compute_hash`
2025-07-03 15:39:29 +00:00
aa16cc9ede Merge branch 'develop' into feature/unit_tests 2025-07-03 14:21:41 +00:00
0efde0e134 clippy fix 2025-07-03 14:21:02 +00:00
b9e553f112 use base64 crate instead of base64ct, no constant time necessary 2025-07-03 14:20:30 +00:00
10 changed files with 269 additions and 45 deletions

99
Cargo.lock generated
View file

@ -85,12 +85,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.1" version = "2.9.1"
@ -350,6 +344,22 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.1" version = "1.1.1"
@ -383,7 +393,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
] ]
[[package]] [[package]]
@ -606,6 +628,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.0" version = "0.8.0"
@ -729,13 +757,19 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.4.6" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.16",
"libredox", "libredox",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@ -777,12 +811,25 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.16",
"libc", "libc",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rustix"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.27" version = "0.23.27"
@ -881,7 +928,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
name = "shrupl" name = "shrupl"
version = "0.1.0-alpha" version = "0.1.0-alpha"
dependencies = [ dependencies = [
"base64ct", "base64",
"blake2b_simd", "blake2b_simd",
"clap", "clap",
"console", "console",
@ -894,6 +941,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
"tempfile",
"thiserror 2.0.12", "thiserror 2.0.12",
"ureq", "ureq",
] ]
@ -944,6 +992,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tempfile"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@ -1117,6 +1178,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.100" version = "0.2.100"
@ -1306,6 +1376,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.1" version = "0.6.1"

View file

@ -5,7 +5,7 @@ edition = "2024"
description = "ShrUpl is a tool to upload files to a Sharry Instance through a public Alias, leveraging the tus protocol" description = "ShrUpl is a tool to upload files to a Sharry Instance through a public Alias, leveraging the tus protocol"
[dependencies] [dependencies]
base64ct = { version = "1.8.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1", default-features = false }
blake2b_simd = "1.0.3" blake2b_simd = "1.0.3"
clap = { version = "4.5.38", features = ["derive"] } clap = { version = "4.5.38", features = ["derive"] }
console = { version = "0.15.11", default-features = false } console = { version = "0.15.11", default-features = false }
@ -21,6 +21,9 @@ serde_json = "1.0.140"
thiserror = "2.0.12" thiserror = "2.0.12"
ureq = { version = "3.0.11", features = ["json"] } ureq = { version = "3.0.11", features = ["json"] }
[dev-dependencies]
tempfile = "3.20.0"
[profile.release] [profile.release]
# Optimize for speed even more aggressively # Optimize for speed even more aggressively
opt-level = "z" opt-level = "z"

View file

@ -1,6 +1,6 @@
use std::{convert::Infallible, fmt, io, time::Duration}; use std::{convert::Infallible, fmt, io, time::Duration};
use base64ct::{Base64UrlUnpadded, Encoding}; use base64::prelude::{BASE64_URL_SAFE_NO_PAD as BASE64URL, Engine};
use blake2b_simd::Params as Blake2b; use blake2b_simd::Params as Blake2b;
use clap::{Parser, builder::TypedValueParser, value_parser}; use clap::{Parser, builder::TypedValueParser, value_parser};
use log::LevelFilter; use log::LevelFilter;
@ -154,6 +154,6 @@ impl Cli {
hasher.update(chk.as_ref()); hasher.update(chk.as_ref());
} }
Base64UrlUnpadded::encode_string(hasher.finalize().as_bytes()) BASE64URL.encode(hasher.finalize())
} }
} }

View file

@ -121,6 +121,18 @@ impl Error {
} }
} }
pub fn is_mismatch<E, A>(&self, has_expected: E, has_actual: A) -> bool
where
String: PartialEq<E> + PartialEq<A>,
{
matches!(
self,
Self::Mismatch { expected, actual }
if *expected == has_expected
&& *actual == has_actual
)
}
#[must_use] #[must_use]
pub fn get_invalid_param(&self) -> Option<&Parameter> { pub fn get_invalid_param(&self) -> Option<&Parameter> {
if let Self::InvalidParameter(p) = self { if let Self::InvalidParameter(p) = self {

View file

@ -59,7 +59,7 @@ impl Checked {
return Err(crate::Error::mismatch("unhashed file", self.path.display())); return Err(crate::Error::mismatch("unhashed file", self.path.display()));
} }
self.hash = Some(super::compute_file_hash(&self.path, self.size, f)?); self.hash = Some(super::compute_hash(&self.path, self.size, f)?);
Ok(()) Ok(())
} }
@ -100,6 +100,11 @@ impl<'t> FileTrait<'t> for Checked {
} }
fn check_hash(&self, on_progress: impl Fn(u64)) -> crate::Result<()> { fn check_hash(&self, on_progress: impl Fn(u64)) -> crate::Result<()> {
super::check_file_hash(&self.path, self.size, self.hash.as_ref(), on_progress) super::check_hash(
&self.path,
self.size,
self.hash.as_ref().map(String::as_str),
on_progress,
)
} }
} }

View file

@ -4,7 +4,7 @@ mod uploading;
use std::{ffi::OsStr, fs, io::Read, path::Path}; use std::{ffi::OsStr, fs, io::Read, path::Path};
use base64ct::{Base64, Encoding}; use base64::prelude::{BASE64_STANDARD_NO_PAD as BASE64, Engine};
use blake2b_simd::Params as Blake2b; use blake2b_simd::Params as Blake2b;
pub use checked::Checked; pub use checked::Checked;
@ -12,12 +12,14 @@ pub use chunk::Chunk;
use log::{debug, warn}; use log::{debug, warn};
pub use uploading::Uploading; pub use uploading::Uploading;
fn compute_hash(path: &Path, size: u64, mut on_progress: impl FnMut(u64)) -> crate::Result<String> {
fn compute_file_hash(path: &Path, size: u64, on_progress: impl Fn(u64)) -> crate::Result<String> {
let mut file = fs::File::open(path)?; let mut file = fs::File::open(path)?;
// Blake2b-512 hasher (64 * 8 bit)
let mut hasher = Blake2b::new().hash_length(64).to_state(); let mut hasher = Blake2b::new().hash_length(64).to_state();
let mut buf = vec![0u8; 4 * 1024 * 1024]; // buffer (4 MiB)
let mut buf = vec![0; 4 * 1024 * 1024];
let mut bytes_read = 0; let mut bytes_read = 0;
loop { loop {
@ -27,6 +29,7 @@ fn compute_file_hash(path: &Path, size: u64, on_progress: impl Fn(u64)) -> crate
} }
hasher.update(&buf[..n]); hasher.update(&buf[..n]);
// `buf` size must be < 2 EiB
bytes_read += n as u64; bytes_read += n as u64;
on_progress(n as u64); on_progress(n as u64);
} }
@ -35,22 +38,22 @@ fn compute_file_hash(path: &Path, size: u64, on_progress: impl Fn(u64)) -> crate
return Err(crate::Error::mismatch(size, bytes_read)); return Err(crate::Error::mismatch(size, bytes_read));
} }
let result = Base64::encode_string(hasher.finalize().as_bytes()); let result = BASE64.encode(hasher.finalize());
debug!("hashed {:?}: {result:?}", path.display()); debug!("hashed {:?}: {result:?}", path.display());
Ok(result) Ok(result)
} }
fn check_file_hash( fn check_hash(
path: &Path, path: &Path,
size: u64, size: u64,
hash: Option<&String>, hash: Option<&str>,
on_progress: impl Fn(u64), on_progress: impl FnMut(u64),
) -> crate::Result<()> { ) -> crate::Result<()> {
let Some(expected) = hash else { let Some(expected) = hash else {
return Err(crate::Error::mismatch("hash", path.display())); return Err(crate::Error::mismatch("hash", path.display()));
}; };
let actual = &compute_file_hash(path, size, on_progress)?; let actual = &compute_hash(path, size, on_progress)?;
if expected == actual { if expected == actual {
debug!("hash matches {expected:?}"); debug!("hash matches {expected:?}");
@ -81,3 +84,126 @@ pub trait FileTrait<'t> {
fn check_hash(&self, on_progress: impl Fn(u64)) -> crate::Result<()>; fn check_hash(&self, on_progress: impl Fn(u64)) -> crate::Result<()>;
} }
#[cfg(test)]
mod tests {
use std::io::Write;
use tempfile::NamedTempFile;
use super::*;
/// Helper to create a temp file from `data`
fn create_file(data: &[u8]) -> NamedTempFile {
let mut tmp = NamedTempFile::new().expect("creating temp file");
tmp.write_all(data).expect("writing to tempfile");
tmp
}
static CASES: [(&[u8], u64); 8] = [
(b"The quick brown fox jumps over the lazy dog", 43), // common pangram
(b"hello world", 11), // simple greeting
(b"", 0), // empty slice
(b"x", 1), // single-byte
(b"0123456789", 10), // numeric ASCII
(b"!@#$%^&*()_+-=[]{};':,.<>/?", 27), // punctuation
(b"RustLang1337", 12), // mixed alphanumeric
(b"foo\0bar\0baz", 11), // embedded nulls
];
static HASHES: [&str; 8] = [
"qK3Uvd39k+SHfSdG5igXsRY2Sh+nvBSNlQkLxzM7NnP4JAHPeqLkyx7NkCluPxTLVBP47Xe+cwRbE5FM3NapGA", // common pangram
"Ahzth5kpbOylV4MquUGlC0oR+DR4zxQfUfkz9lOrn7zAWgN83b7QbjCb8zSULE5YzfGkbiN5EczX/Pl4fLx/0A", // simple greeting
"eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg", // empty slice
"CQk3etNREMr7KQnhhWcrfyco0fUJT4rWjW+sYnS/H0mUhagOo2TATtAG0pRZ6jy3xgAoDi+D4DJSmQb4iuMNCg", // single-byte
"UqCSwAW2Ib1X5QGgrtlQp2/vuwDQeqQ9rdb1NALMJUE3SfDTxi6MoKfbrjRIQa3qUdU/i2HZaaFdSmMYtXa4rA", // numeric ASCII
"Sr91qmX4R/Ly4HsJh5eiG3S1tuO81kwV0KPfRpn1j4jjrQoGL2I+SeKfcGvpXu3l/rfhGdJHF8ei775ZzdgK3Q", // punctuation
"Ox+zobaUmB8Ps410/TGOtjjLIJKaMUCwG/iFLNXjwRShuJAmtvQcK9Ahc9+SfD4Ci67HyPPorl7NGjN6LRrmlQ", // mixed alphanumeric
"a3rsGWE2kfvN6e2sVhioWP9NOmwLK9trzjc/GKXTPvvsiagiRSHMjlg5jy+bMepip68Pv69dY8TvTSFZES5Jzw", // embedded nulls
];
#[test]
fn compute_hash_as_expected() {
for (&(content, size), expected_hash) in CASES.iter().zip(HASHES) {
// to capture progress updates from `compute_hash`
let file = create_file(content);
let mut read_total = 0;
let callback = |n| read_total += n;
let hash = compute_hash(file.path(), size, callback).expect("hash should succeed");
assert_eq!(hash, expected_hash);
assert_eq!(read_total, size);
}
}
#[test]
fn hash_size_mismatch() {
let bad_sizes = [
36, // common pangram
12, // simple greeting
1, // empty slice
0, // single-byte
9, // numeric ASCII
24, // punctuation
13, // mixed alphanumeric
10, // embedded nulls
];
for (&(content, good_size), bad_size) in CASES.iter().zip(bad_sizes) {
let file = create_file(content);
let callback = drop;
{
let err = compute_hash(file.path(), bad_size, callback)
.expect_err("compute_hash should report a mismatch");
// check error
assert!(err.is_mismatch(bad_size.to_string(), good_size.to_string()));
}
{
let err = check_hash(file.path(), bad_size, Some("foobar"), callback)
.expect_err("check_hash should report a mismatch");
// check error
assert!(err.is_mismatch(bad_size.to_string(), good_size.to_string()));
}
}
}
#[test]
fn hash_value_none() {
for (content, size) in CASES {
let file = create_file(content);
let callback = drop;
let err = check_hash(file.path(), size, None, callback)
.expect_err("check_hash should report a mismatch");
// check error
assert!(err.is_mismatch("hash", file.path().display().to_string()));
}
}
#[test]
fn hash_value_mismatch() {
let bad_hashes = [
"invalid9k+SHfSdG5igXsRY2Sh+nvBSNlQkLxzM7NnP4JAHPeqLkyx7NkCluPxTLVBP47Xe+cwRbE5FM3NapGA", // common pangram
"", // simple greeting
"eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiG/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg", // empty slice
"Hash", // single-byte
];
for ((&(content, size), good_hash), bad_hash) in CASES.iter().zip(HASHES).zip(bad_hashes) {
let file = create_file(content);
let callback = drop;
let err = check_hash(file.path(), size, Some(bad_hash), callback)
.expect_err("check_hash should report a mismatch");
// check error
assert!(err.is_mismatch(bad_hash, good_hash));
}
}
}

View file

@ -107,6 +107,11 @@ impl<'t> FileTrait<'t> for Uploading {
} }
fn check_hash(&self, on_progress: impl Fn(u64)) -> crate::Result<()> { fn check_hash(&self, on_progress: impl Fn(u64)) -> crate::Result<()> {
super::check_file_hash(&self.path, self.size, self.hash.as_ref(), on_progress) super::check_hash(
&self.path,
self.size,
self.hash.as_ref().map(String::as_str),
on_progress,
)
} }
} }

View file

@ -106,7 +106,7 @@ mod tests {
#[test] #[test]
fn basic_traits_working() { fn basic_traits_working() {
let inputs = vec![ let inputs = [
"", "",
"abcd", "abcd",
"12345", "12345",
@ -142,7 +142,7 @@ mod tests {
#[test] #[test]
fn valid_urls_produce_expected_file_id() { fn valid_urls_produce_expected_file_id() {
// a handful of validlooking URLs // a handful of validlooking URLs
let cases = vec![ let cases = [
( (
"http://example.com/api/v2/alias/upload/SID123/files/tus/FID456", "http://example.com/api/v2/alias/upload/SID123/files/tus/FID456",
"FID456", "FID456",
@ -169,7 +169,7 @@ mod tests {
#[test] #[test]
fn invalid_urls_return_error() { fn invalid_urls_return_error() {
let bad_inputs = vec![ let bad_inputs = [
// missing /api/v2/alias/upload // missing /api/v2/alias/upload
"http://example.com/files/tus/FID", "http://example.com/files/tus/FID",
// missing /files/tus // missing /files/tus
@ -185,16 +185,10 @@ mod tests {
for bad in bad_inputs { for bad in bad_inputs {
let err = FileID::try_from(bad.to_string()).expect_err("URL should not parse"); let err = FileID::try_from(bad.to_string()).expect_err("URL should not parse");
// make sure it's the Mismatch variant, and that it contains the original input // make sure it's the Mismatch variant, and that it contains the original input
match err { assert!(err.is_mismatch(
crate::Error::Mismatch { expected, actual } => { "<proto>://<host>/api/v2/alias/upload/<share>/files/tus/<file>",
assert_eq!( bad
expected, "<proto>://<host>/api/v2/alias/upload/<share>/files/tus/<file>", ));
"Error should output expected format"
);
assert_eq!(actual, bad, "Error should echo back the input");
}
_ => panic!("Expected Error::Mismatch for input `{bad}` but got {err:?}"),
}
} }
} }
} }

View file

@ -59,7 +59,7 @@ mod tests {
#[test] #[test]
fn nsreq_new_sets_fields_correctly() { fn nsreq_new_sets_fields_correctly() {
let cases: Vec<(&str, u32)> = vec![ let cases = [
// simple ASCII name, small view count // simple ASCII name, small view count
("alice", 1), ("alice", 1),
// underscores, mid-range views // underscores, mid-range views
@ -95,7 +95,7 @@ mod tests {
fn nsreq_new_allows_setting_description() { fn nsreq_new_allows_setting_description() {
let longstr = "y".repeat(256); let longstr = "y".repeat(256);
let cases = vec![ let cases = [
// simple alphanumeric // simple alphanumeric
"A simple test user", "A simple test user",
// whitespace & punctuation // whitespace & punctuation

View file

@ -43,7 +43,7 @@ impl From<String> for Uri {
SHARRY_URI_RE.captures(value).map(|caps| { SHARRY_URI_RE.captures(value).map(|caps| {
let captured = |name| { let captured = |name| {
caps.name(name) caps.name(name)
.expect(&format!("{name} not captured")) .unwrap_or_else(|| panic!("{name} not captured"))
.as_str() .as_str()
.to_string() .to_string()
}; };
@ -104,7 +104,7 @@ mod tests {
#[test] #[test]
fn basic_traits_working() { fn basic_traits_working() {
let cases = vec![ let cases = [
// simple http host // simple http host
"http://example.com", "http://example.com",
// https host with port // https host with port
@ -122,7 +122,7 @@ mod tests {
#[test] #[test]
fn valid_urls_produce_expected_uri() { fn valid_urls_produce_expected_uri() {
let cases = vec![ let cases = [
// simple http host // simple http host
("http://example.com", "http://example.com"), ("http://example.com", "http://example.com"),
// https host with port // https host with port
@ -143,7 +143,7 @@ mod tests {
#[test] #[test]
fn invalid_urls_passed_through() { fn invalid_urls_passed_through() {
let cases = vec![ let cases = [
// missing “://” // missing “://”
"http:/example.com", "http:/example.com",
// missing scheme // missing scheme
@ -165,7 +165,7 @@ mod tests {
#[test] #[test]
fn test_endpoint() { fn test_endpoint() {
let cases = vec![ let cases = [
// simple path // simple path
("path/to/something", "/api/v2/path/to/something"), ("path/to/something", "/api/v2/path/to/something"),
// underscores, hyphens, dots // underscores, hyphens, dots