aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorA Farzat <a@farzat.xyz>2026-02-14 16:59:20 +0300
committerA Farzat <a@farzat.xyz>2026-02-14 16:59:20 +0300
commitfa7aa3ab48d1694fce44f7454f6331757677dde3 (patch)
treecc5b96b6124d1be50dab5ad6309ae1c51581c2a3
parentca356049faf28ab48b73d04d80d4d11236d3abc6 (diff)
downloadsafaribooks-rs-fa7aa3ab48d1694fce44f7454f6331757677dde3.tar.gz
safaribooks-rs-fa7aa3ab48d1694fce44f7454f6331757677dde3.zip
Add unit tests for epub.rs
-rw-r--r--Cargo.lock59
-rw-r--r--Cargo.toml6
-rw-r--r--src/epub.rs126
3 files changed, 190 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e971da4..f8e6639 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -301,6 +301,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -688,6 +704,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
name = "litemap"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -814,6 +836,15 @@ dependencies = [
]
[[package]]
+name = "quick-xml"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2e3bf4aa9d243beeb01a7b3bc30b77cfe2c44e24ec02d751a7104a53c2c49a1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -988,6 +1019,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
+name = "rustix"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "rustls"
version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1075,9 +1119,11 @@ dependencies = [
"anyhow",
"clap",
"colored",
+ "quick-xml",
"reqwest",
"serde",
"serde_json",
+ "tempfile",
"tokio",
"tracing",
"tracing-subscriber",
@@ -1261,6 +1307,19 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.4",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index efd6672..6fb629a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,10 +7,14 @@ edition = "2024"
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
colored = "3.1"
-reqwest = { version = "0.13", default-features = false, features = ["gzip", "json", "rustls"] }
+reqwest = { version = "0.13", default-features = false, features = ["deflate", "gzip", "json", "rustls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.49", features = ["rt-multi-thread", "macros"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
unicode-normalization = "0.1"
+
+[dev-dependencies]
+quick-xml = "0.39.0"
+tempfile = "3.25.0"
diff --git a/src/epub.rs b/src/epub.rs
index 63dfc4b..61c9003 100644
--- a/src/epub.rs
+++ b/src/epub.rs
@@ -132,3 +132,129 @@ fn truncate_utf8_by_byte(s: &str, max_bytes: usize) -> &str {
&s[..end]
}
+
+#[cfg(test)]
+mod tests {
+ use super::EpubSkeleton;
+ use tempfile::TempDir;
+ use quick_xml::{Reader, events::Event};
+ use std::fs;
+
+ /// Make a temp directory with a predictable prefix.
+ fn temp(label: &str) -> TempDir {
+ tempfile::Builder::new()
+ .prefix(&format!("safaribooks-rs-{}", label))
+ .tempdir()
+ .unwrap_or_else(|_| panic!("Create tempdir with label: {}", label))
+ }
+
+ #[test]
+ fn initialize_skeleton() {
+ // GIVEN
+ let tmp = temp("initialize");
+ let base = tmp.path();
+ let skel = EpubSkeleton::plan(base, "A Title", "1234567890123");
+
+ // WHEN
+ skel.initialize().expect("Initialize skeleton");
+
+ // THEN: directory structure exists
+ assert!(skel.root.exists(), "Root dir missing: {}", skel.root.display());
+ assert!(skel.oebps.exists(), "OEBPS dir missing: {}", skel.oebps.display());
+ assert!(skel.meta_inf.exists(), "META-INF dir missing: {}", skel.meta_inf.display());
+ }
+
+ #[test]
+ fn mimetype_exact() {
+ // GIVEN
+ let tmp = temp("mimetype");
+ let base = tmp.path();
+ let skel = EpubSkeleton::plan(base, "A Title", "1234567890123");
+
+ // WHEN
+ skel.create_dirs().expect("Create skeleton dirs");
+ skel.write_mimetype().expect("Write mimetype");
+
+ // THEN: file exists
+ let mimetype = skel.root.join("mimetype");
+ assert!(mimetype.exists(), "Mimetype file not found");
+
+ // mimetype has *exact* bytes with *no* trailing newline.
+ let bytes = fs::read(&mimetype).expect("Read mimetype");
+ assert_eq!(
+ bytes.as_slice(),
+ b"application/epub+zip",
+ "mimetype must be exactly 'application/epub+zip' with NO trailing newline"
+ );
+ }
+
+ #[test]
+ fn container_xml_well_formed() {
+ // GIVEN
+ let tmp = temp("container");
+ let base = tmp.path();
+ let skel = EpubSkeleton::plan(base, "Another Title", "9876543210");
+
+ // WHEN
+ skel.create_dirs().expect("Create skeleton dirs");
+ skel.write_container_xml().expect("Write container.xml");
+
+ // THEN: file exists
+ let container = skel.meta_inf.join("container.xml");
+ assert!(container.exists(), "META-INF/container.xml not found");
+
+ // Parse with quick-xml to ensure it is well-formed and to inspect elements.
+ let xml = fs::read_to_string(&container).expect("Read container.xml");
+ let mut reader = Reader::from_str(xml.trim());
+
+ // Walk events; ensure <container> and expected <rootfile> are present with correct attributes.
+ let mut saw_container = false;
+ let mut saw_rootfiles = false;
+ let mut saw_rootfile_ok = false;
+
+ let mut buf = Vec::<u8>::new();
+ loop {
+ match reader.read_event_into(&mut buf) {
+ Ok(Event::Start(e) | Event::Empty(e)) => {
+ let name_tmp = e.name();
+ let name = name_tmp.as_ref();
+ if name == b"container" {
+ saw_container = true;
+ } else if name == b"rootfiles" {
+ saw_rootfiles = true;
+ } else if name == b"rootfile" {
+ // Check attributes on rootfile
+ let mut full_path_ok = false;
+ let mut media_type_ok = false;
+
+ for a in e.attributes().flatten() {
+ if a.key.as_ref() == b"full-path" && a.value.as_ref() == b"OEBPS/content.opf" {
+ full_path_ok = true;
+ }
+ else if a.key.as_ref() == b"media-type"
+ && a.value.as_ref() == b"application/oebps-package+xml"
+ {
+ media_type_ok = true;
+ }
+ }
+ if full_path_ok && media_type_ok {
+ saw_rootfile_ok = true;
+ }
+ }
+ }
+ Ok(Event::Eof) => break,
+ Ok(_) => {}
+ Err(e) => panic!("XML parse error at position {}: {e}", reader.buffer_position()),
+ }
+ buf.clear();
+ }
+
+ assert!(saw_container, "container.xml is missing <container> root element");
+ assert!(saw_rootfiles, "container.xml is missing <rootfiles> element");
+ assert!(
+ saw_rootfile_ok,
+ "container.xml <rootfile> must have full-path='OEBPS/content.opf' \
+ and media-type='application/oebps-package+xml'"
+ );
+ }
+}