From fa7aa3ab48d1694fce44f7454f6331757677dde3 Mon Sep 17 00:00:00 2001 From: A Farzat Date: Sat, 14 Feb 2026 16:59:20 +0300 Subject: Add unit tests for epub.rs --- Cargo.lock | 59 ++++++++++++++++++++++++++++ Cargo.toml | 6 ++- src/epub.rs | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index e971da4..f8e6639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,22 @@ version = "1.0.5" 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" @@ -687,6 +703,12 @@ version = "0.2.181" 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" @@ -813,6 +835,15 @@ dependencies = [ "unicode-ident", ] +[[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" @@ -987,6 +1018,19 @@ version = "2.1.1" 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" @@ -1075,9 +1119,11 @@ dependencies = [ "anyhow", "clap", "colored", + "quick-xml", "reqwest", "serde", "serde_json", + "tempfile", "tokio", "tracing", "tracing-subscriber", @@ -1260,6 +1306,19 @@ dependencies = [ "syn", ] +[[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" 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 and expected 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::::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 root element"); + assert!(saw_rootfiles, "container.xml is missing element"); + assert!( + saw_rootfile_ok, + "container.xml must have full-path='OEBPS/content.opf' \ + and media-type='application/oebps-package+xml'" + ); + } +} -- cgit v1.2.3-70-g09d2