diff options
| author | A Farzat <a@farzat.xyz> | 2026-02-14 16:59:20 +0300 |
|---|---|---|
| committer | A Farzat <a@farzat.xyz> | 2026-02-14 16:59:20 +0300 |
| commit | fa7aa3ab48d1694fce44f7454f6331757677dde3 (patch) | |
| tree | cc5b96b6124d1be50dab5ad6309ae1c51581c2a3 | |
| parent | ca356049faf28ab48b73d04d80d4d11236d3abc6 (diff) | |
| download | safaribooks-rs-fa7aa3ab48d1694fce44f7454f6331757677dde3.tar.gz safaribooks-rs-fa7aa3ab48d1694fce44f7454f6331757677dde3.zip | |
Add unit tests for epub.rs
| -rw-r--r-- | Cargo.lock | 59 | ||||
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | src/epub.rs | 126 |
3 files changed, 190 insertions, 1 deletions
@@ -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" @@ -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'" + ); + } +} |
