From e657eb65134a41687ee2becb380eb3f5c2626c8b Mon Sep 17 00:00:00 2001 From: jutty Date: Thu, 25 Dec 2025 23:57:22 -0300 Subject: [PATCH] Add tests for all but the content syntax parser module --- .clippy.toml | 2 + .justfile | 224 +++++++++++---------- Cargo.lock | 251 +++++++++++++++++++++++- Cargo.toml | 11 +- src/dev.rs | 39 ++++ src/router.rs | 122 ++++++++++++ src/router/handlers/error.rs | 30 +++ src/router/handlers/fixed.rs | 65 +++++- src/router/handlers/graph.rs | 41 ++++ src/router/handlers/navigation.rs | 35 ++++ src/router/handlers/raw.rs | 32 ++- src/router/handlers/template.rs | 113 ++++++++++- src/syntax/command.rs | 57 +++++- src/syntax/serial.rs | 90 ++++++++- src/types.rs | 81 ++++++++ tests/mocks/bad_graph/static/graph.toml | 3 + tests/mocks/good_json/graph.json | 13 ++ 17 files changed, 1077 insertions(+), 132 deletions(-) create mode 100644 .clippy.toml create mode 100644 tests/mocks/bad_graph/static/graph.toml create mode 100644 tests/mocks/good_json/graph.json diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 0000000..0358cdb --- /dev/null +++ b/.clippy.toml @@ -0,0 +1,2 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true diff --git a/.justfile b/.justfile index c971f27..1ad717b 100644 --- a/.justfile +++ b/.justfile @@ -1,154 +1,176 @@ _default: @just --list +watch_cmd := "watchexec -qc -r -e rs,toml,html --color always -- " +cover_cmd := 'cargo llvm-cov --ignore-filename-regex "main\.rs|dev\.rs"' +just_cmd := 'just --unstable --timestamp --explain --command-color green' + # DEV +# Start server +[group: 'develop'] +run: + cargo run -- --hostname localhost --port 3003 + +alias r := run + # Build on changes -[group('dev')] -serve-watch: - watchexec -q -c -e rs,toml,html -r cargo run -- -p 3003 -h localhost +[group: 'develop'] +run-watch: + {{ watch_cmd }} {{ just_cmd }} run -alias sw := serve-watch -alias dev := serve-watch -alias d := serve-watch +alias rw := run-watch +alias dev := run-watch +alias d := run-watch -[group('dev')] -serve-watch-interface: - watchexec -qr -c -w . -w ../interface -e rs,toml,html cargo run \ - -- -h localhost -p 3001 -g ../interface/graph.toml +# Run all assessments on changes +[group: 'develop'] +verify-watch: + {{ watch_cmd }} {{ just_cmd }} verify -alias swi := serve-watch-interface -alias dev-interface := serve-watch-interface -alias di := serve-watch-interface +alias vw := verify-watch # Run tests on changes -[group('dev')] +[group: 'develop'] test-watch: - bacon --job test + {{ watch_cmd }} {{ just_cmd }} test alias tw := test-watch +# Run tests with coverage reports on changes +[group: 'develop'] +cover-watch: + {{ watch_cmd }} {{ just_cmd }} cover-report + +alias ow := cover-watch + # Run cargo check on changes -[group('dev')] +[group: 'develop'] check-watch: - bacon --job check + {{ watch_cmd }} {{ just_cmd }} check alias cw := check-watch -# Format check on changes -[group('dev')] -format-watch: - bacon --job fmt-check - -alias fw := format-watch - # Lint on changes -[group('dev')] +[group: 'develop'] lint-watch: - bacon --job clippy + {{ watch_cmd }} {{ just_cmd }} lint alias lw := lint-watch -# Check before push -[group('dev')] -push: check +# Assess formatting on changes +[group: 'develop'] +format-watch: + {{ watch_cmd }} {{ just_cmd }} format-assess + +alias fw := format-watch + +# Format all files +[group: 'develop'] +format: + cargo fmt + +alias f := format + +# Verify before push +[group: 'develop'] +push: verify git push alias p := push -# RUN +# ANALYSIS -# Start server -[group('run')] -serve: - cargo run -- --hostname localhost --port 3003 +# Run all analysis +[group: 'assess'] +verify: format-assess lint check test cover-assess -alias s := serve +alias v := verify + +# Assess coverage +[group: 'assess'] +cover-assess: + {{ cover_cmd }} --fail-under-regions 90 report + +# Assess formatting +[group: 'assess'] +format-assess: + cargo fmt -- --check + +alias fc := format-assess + +# Lint with Clippy +[group: 'assess'] +lint: + cargo clippy + +alias l := lint + +# Run cargo check +[group: 'assess'] +check: + cargo check --workspace + +alias c := check + +# Run tests +[group: 'assess'] +test: + cargo test -- --skip 'serial_tests::' + cargo test -- --test 'serial_tests::' --test-threads 1 + +alias t := test + +# Run tests with coverage +[group: 'assess'] +cover: + {{ cover_cmd }} --no-report -- --skip 'serial_tests::' + {{ cover_cmd }} --no-report -- --test 'serial_tests::' --test-threads 1 + +alias o := cover + +## COVER + +# Make coverage report +[group: 'cover'] +cover-report: cover + {{ cover_cmd }} report --html + {{ cover_cmd }} report + +alias or := cover-report + +# Open coverage report +[group: 'cover'] +cover-open: cover + {{ cover_cmd }} report --open + +alias oo := cover-open # BUILD # Build project with Cargo -[group('build')] +[group: 'build'] build: cargo build alias b := build # Cleanup build artifacts -[group('build')] +[group: 'build'] clean: cargo clean alias cl := clean -# Clean, build, run checks -[group('build')] -full-build: clean build check +# Clean, run assessments, release build +[group: 'build'] +full-build: clean verify release-build alias fb := full-build # Release build -[group('build')] -release-build: +[group: 'build'] +release-build: verify cargo build --release alias rb := release-build - -# CHECKS - -# Lint, check formatting and run tests -[group('checks')] -check: format-check lint cargo-check test - -alias c := check - -# Run cargo check -[group('checks')] -cargo-check: - cargo check --workspace - -alias cc := cargo-check - -# Lint with Clippy -[group('checks')] -lint: - cargo clippy - -alias l := lint - -# Check formatting without changing files -[group('checks')] -format-check: - cargo fmt -- --check - -alias fc := format-check - -# Run tests -[group('checks')] -test: - cargo test - -alias t := test - -# Run tests with coverage -[group('checks')] -cover: - cargo llvm-cov test - -alias cv := cover - -# Open test coverage report -[group('checks')] -cover-open: - cargo llvm-cov --open - -alias cvo := cover-open - -# FORMATTING - -# Format all files -[group('checks')] -format: - cargo fmt - -alias f := format diff --git a/Cargo.lock b/Cargo.lock index c967ee9..7e88c5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -84,6 +90,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.10.0" @@ -185,6 +197,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -246,6 +267,8 @@ dependencies = [ "tera", "tokio", "toml", + "tower", + "ureq", ] [[package]] @@ -260,6 +283,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -552,6 +585,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -772,6 +815,55 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -885,6 +977,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "siphasher" version = "1.0.1" @@ -917,6 +1015,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.111" @@ -1092,6 +1196,47 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "version_check" version = "0.9.5" @@ -1159,6 +1304,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -1227,13 +1381,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1245,6 +1408,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1252,58 +1431,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -1335,3 +1562,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/Cargo.toml b/Cargo.toml index 799dcc9..c29ea33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,9 @@ documentation = "https://en.jutty.dev/node/Documentation" edition = "2024" rust-version= "1.91.1" +[features] +serial-tests = [] + [dependencies] axum = "0.8.7" tokio = { version = "1.48.0", features = ["rt-multi-thread"] } @@ -19,9 +22,13 @@ serde_json = "1.0.145" serde = { version = "1.0.228", features = ["derive"] } toml = "0.9.8" +[dev-dependencies] +ureq = "3" +tower = { version = "0.5.2", features = ["util"] } + [lints.rust] # levels: allow, expect, warn, force-warn, deny, forbid -unsafe_code = { level = "forbid", priority = 99 } +unsafe_code = { level = "deny", priority = 99 } unused = { level = "warn", priority = 10 } let_underscore= { level = "warn", priority = 10 } nonstandard-style = "warn" @@ -141,7 +148,6 @@ wildcard_imports = "warn" zero_sized_map_values = "warn" # restrictive -allow_attributes = "forbid" arithmetic_side_effects = "warn" as_conversions = "warn" as_pointer_underscore = "warn" @@ -155,7 +161,6 @@ expect_used = "warn" filetype_is_file = "warn" float_cmp_const = "warn" fn_to_numeric_cast_any = "warn" -get_unwrap = "warn" if_then_some_else_none = "warn" indexing_slicing = "warn" infinite_loop = "warn" diff --git a/src/dev.rs b/src/dev.rs index cc2f141..743ade7 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -52,3 +52,42 @@ macro_rules! log { }}; } + +#[cfg(test)] +mod tests { + + fn run_in_debug_level(level: &str) { + #[allow(unsafe_code)] + unsafe { + std::env::set_var("DEBUG", level); + log!("Debug is set to {level}"); + } + } + + #[test] + fn debug_var_set() { + for level in 0..9 { + run_in_debug_level(&level.to_string()); + } + run_in_debug_level(""); + run_in_debug_level("駄目!"); + } + + #[test] + fn trait_stripping() { + pub trait Loggable { + fn test(&self); + } + + struct Logger {} + + impl Loggable for Logger { + fn test(&self) { + log!("This is inside a trait implementation"); + } + } + + let logger = Logger {}; + logger.test(); + } +} diff --git a/src/router.rs b/src/router.rs index 735ca6b..e7aa60c 100644 --- a/src/router.rs +++ b/src/router.rs @@ -54,3 +54,125 @@ pub fn new(graph: &Graph) -> Router { router } + +#[cfg(test)] +mod tests { + use crate::{ + syntax::serial::populate_graph, + types::{Config, Meta}, + }; + + use super::*; + use axum::{ + body::Body, + http::{Request, StatusCode}, + response::Response, + }; + use tower::ServiceExt as _; + + async fn request(uri: &str, config: Option<&Config>) -> Response { + let default_graph = populate_graph(); + let graph = Graph { + meta: Meta { + config: config + .map(|c| c.to_owned()) + .unwrap_or(default_graph.meta.config), + ..default_graph.meta + }, + ..default_graph + }; + let router = new(&graph); + + router + .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) + .await + .unwrap() + } + + #[tokio::test] + async fn smoke() { + let router = axum::Router::new(); + let response = router + .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn routes() { + let routes = [ + "/", + "/about", + "/tree", + "/node/Syntax", + "/static/style.css", + "/static/favicon.svg", + "/graph/json", + "/graph/toml", + ]; + + for route in routes { + let response = request(route, None).await; + assert_eq!(response.status(), StatusCode::OK); + } + } + + #[tokio::test] + async fn no_about_page() { + let config = Config { + about: false, + ..populate_graph().meta.config + }; + + let response = request("/about", Some(&config)).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn no_tree_page() { + let config = Config { + tree: false, + ..populate_graph().meta.config + }; + + let response = request("/tree", Some(&config)).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn no_toml_raw_graph() { + let config = Config { + raw_toml: false, + ..populate_graph().meta.config + }; + + let response = request("/graph/toml", Some(&config)).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn no_json_raw_graph() { + let config = Config { + raw_json: false, + ..populate_graph().meta.config + }; + + let response = request("/graph/json", Some(&config)).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn no_raw_graph() { + let config = Config { + raw: false, + ..populate_graph().meta.config + }; + + let toml_response = request("/graph/toml", Some(&config)).await; + assert_eq!(toml_response.status(), StatusCode::NOT_FOUND); + let json_response = request("/graph/json", Some(&config)).await; + assert_eq!(json_response.status(), StatusCode::NOT_FOUND); + } +} diff --git a/src/router/handlers/error.rs b/src/router/handlers/error.rs index e017e4d..f6c61e6 100644 --- a/src/router/handlers/error.rs +++ b/src/router/handlers/error.rs @@ -56,3 +56,33 @@ pub async fn not_found() -> Response { Some("The page you tried to access could not be found."), ) } + +#[cfg(test)] +mod tests { + use axum::{ + http::{StatusCode}, + }; + use super::*; + + #[tokio::test] + async fn not_found() { + let response = super::not_found().await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn internal_error() { + assert!(by_code(Some(201), None).status() == 201); + assert!(by_code(Some(304), None).status() == 304); + assert!(by_code(Some(418), None).status() == 418); + assert!(by_code(Some(505), None).status() == 505); + } + + #[test] + fn custom_message() { + let pattern = "sibPtt0mvHPWS9HQ0YBQfGu8cUs954LZ"; + let body = make_body(Some(501), Some(pattern)); + assert!(body.contains(pattern)); + assert!(!body.contains(&pattern.chars().rev().collect::())); + } +} diff --git a/src/router/handlers/fixed.rs b/src/router/handlers/fixed.rs index 6406533..90995bd 100644 --- a/src/router/handlers/fixed.rs +++ b/src/router/handlers/fixed.rs @@ -25,11 +25,7 @@ pub async fn file(file_path: &str, content_type: &str) -> Response { let header = header::CONTENT_TYPE; if let Ok(header_value) = HeaderValue::from_str(content_type) { - if let Some(h) = response.headers_mut().insert(header, header_value) { - log!( - "Overwrote existing header {h:?} because a header for the same key existed" - ); - } + response.headers_mut().append(header, header_value); } else { log!("Failed to create content type header value from {content_type}"); } @@ -43,15 +39,70 @@ pub async fn serial(format: &Format) -> Response { let body = serialize_graph(format, &graph); match *format { - Format::Toml => handlers::raw::make_response( + Format::TOML => handlers::raw::make_response( &body, 200, &[(header::CONTENT_TYPE, "text/plain")], ), - Format::Json => handlers::raw::make_response( + Format::JSON => handlers::raw::make_response( &body, 200, &[(header::CONTENT_TYPE, "application/json")], ), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn serial_toml() { + let response = serial(&Format::TOML).await; + assert!(response.status() == 200); + } + + #[tokio::test] + async fn serial_toml_content_type() { + let response = serial(&Format::TOML).await; + assert!( + response.headers().get(header::CONTENT_TYPE).unwrap() + == "text/plain" + ); + } + + #[tokio::test] + async fn serial_json_content_type() { + let response = serial(&Format::JSON).await; + assert!( + response.headers().get(header::CONTENT_TYPE).unwrap() + == "application/json" + ); + } + + #[tokio::test] + async fn file_valid_header() { + let payload = "y1mgMhjeIMFsRNZ1tskP52DfWuvhvbRP"; + let response = file("./static/graph.toml", payload).await; + assert_eq!( + response.headers().get(header::CONTENT_TYPE).unwrap(), + payload + ); + } + + #[tokio::test] + async fn file_invalid_header() { + let response = file("./static/graph.toml", "\n").await; + println!("{response:#?}"); + assert!(response.headers().get(header::CONTENT_TYPE).is_none()); + } + + #[tokio::test] + #[should_panic( + expected = "Failed to read IvnhZhdHb1xDnUw4hYDDNIERoaOojkiu \ + contents: No such file or directory (os error 2)" + )] + async fn file_invalid_path() { + drop(file("IvnhZhdHb1xDnUw4hYDDNIERoaOojkiu", "text/plain").await); + } +} diff --git a/src/router/handlers/graph.rs b/src/router/handlers/graph.rs index a3d3f1e..7ce2858 100644 --- a/src/router/handlers/graph.rs +++ b/src/router/handlers/graph.rs @@ -40,3 +40,44 @@ pub async fn node(Path(id): Path) -> Response { not_found, ) } + +#[cfg(test)] +mod tests { + use axum::{ + http::{HeaderName, StatusCode}, + }; + + use super::*; + + #[tokio::test] + async fn syntax() { + let response = node(Path("Syntax".to_string())).await; + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn syntax_content_type() { + let response = node(Path("Syntax".to_string())).await; + assert!( + response + .headers() + .get(HeaderName::from_static("content-type"),) + .unwrap() + .to_str() + .unwrap() + == "text/html" + ); + } + + #[tokio::test] + async fn not_found() { + let response = node(Path("InexistentNode".to_string())).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn redirect() { + let response = node(Path("syntax".to_string())).await; + assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT); + } +} diff --git a/src/router/handlers/navigation.rs b/src/router/handlers/navigation.rs index 71655c2..f3a2602 100644 --- a/src/router/handlers/navigation.rs +++ b/src/router/handlers/navigation.rs @@ -29,3 +29,38 @@ pub async fn search(Form(query): Form) -> Redirect { pub struct Query { node: String, } + +#[cfg(test)] +mod tests { + use axum::{ + http::{StatusCode}, + }; + use super::*; + + #[tokio::test] + async fn search_redirect() { + let query = Form(Query { + node: String::from("duZzBrgCzMhVY15wehxasezsGNatOKIq"), + }); + let response = search(query).await; + assert!(response.status_code() == StatusCode::PERMANENT_REDIRECT); + } + + #[tokio::test] + async fn about_page_ok() { + let response = page("about.html").await; + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn tree_page_ok() { + let response = page("tree.html").await; + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn inexistent_page_error() { + let response = page("HBvcwqT8wLk6hxk1GdvNcEzJ6IiZ2Fod").await; + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + } +} diff --git a/src/router/handlers/raw.rs b/src/router/handlers/raw.rs index 578add3..9ba49ad 100644 --- a/src/router/handlers/raw.rs +++ b/src/router/handlers/raw.rs @@ -27,9 +27,39 @@ pub(in crate::router::handlers) fn make_response( ); } } else { - log!("Failed to wrap header value {}", header.1); + log!("Failed to create header value from {}", header.1); } } response } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn repeated_header() { + let headers = [ + (header::ACCEPT, "Not really"), + (header::ACCEPT, "This again?"), + ]; + let response = make_response("", 418, &headers); + assert!(response.headers().get_all(header::ACCEPT).iter().count() == 1); + assert_eq!( + response + .headers() + .get(header::ACCEPT) + .unwrap() + .to_str() + .unwrap(), + "This again?", + ); + } + + #[test] + fn invalid_header() { + let response = make_response("", 418, &[(header::MAX_FORWARDS, "\n")]); + assert!(response.headers().get(header::MAX_FORWARDS).is_none()); + } +} diff --git a/src/router/handlers/template.rs b/src/router/handlers/template.rs index 7ce6e52..3b221ef 100644 --- a/src/router/handlers/template.rs +++ b/src/router/handlers/template.rs @@ -27,14 +27,9 @@ pub(in crate::router::handlers) fn render( error_message: Option, ) -> (String, u16) { // TODO just return an Option/String> here - let tera = match tera::Tera::new(concat!( - env!("CARGO_MANIFEST_DIR"), - "/templates/**/*" - )) { + let tera = match tera::Tera::new("./templates/**/*") { Ok(t) => t, Err(e) => { - let early_error_message = format!("{e:#?}"); - log!("{}", early_error_message); return (emergency_wrap(&e), 500); }, }; @@ -73,7 +68,8 @@ pub(in crate::router::handlers) fn render( } } -fn emergency_wrap(message: &tera::Error) -> String { +fn emergency_wrap(error: &tera::Error) -> String { + log!("{error:#?}"); format!( r#" @@ -93,7 +89,7 @@ fn emergency_wrap(message: &tera::Error) -> String {

Early Pre-Templating Error

This normally indicates a malformed template.

-            {message}
+            {error:#?}
             

If you haven't modified templates, plese consider @@ -104,3 +100,104 @@ fn emergency_wrap(message: &tera::Error) -> String { "# ) } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn by_filename_forced_error() { + let response = + by_filename("index.html", &tera::Context::new(), 418, None, true); + assert_eq!(response.status(), 418); + } + + #[test] + fn by_filename_index() { + let response = + by_filename("index.html", &tera::Context::new(), 418, None, false); + assert_eq!(response.status(), 200); + } + + #[test] + fn by_filename_file_not_found() { + let response = by_filename( + "bwbl3BnWsluIgbO2NV9t3vtihwcjuF6t", + &tera::Context::new(), + 418, + None, + false, + ); + assert_eq!(response.status(), 500); + } + + #[test] + fn by_filename_empty() { + let response = by_filename("", &tera::Context::new(), 418, None, false); + assert_eq!(response.status(), 500); + } + + #[test] + fn render_with_context() { + let payload = "dBgIw8DnNHxJojiXzu445qUC4UpxwZCy"; + let mut context = tera::Context::new(); + let node = crate::types::Node::new(Some(payload.to_string())); + let graph = crate::syntax::serial::populate_graph(); + context.insert("node", &node); + context.insert("text", &crate::syntax::content::parse(&node.text)); + context.insert("incoming", &graph.incoming.get(&node.id)); + context.insert("config", &graph.meta.config.parse_text()); + let (body, status) = render("node.html", &context, None); + assert_eq!(status, 200); + assert!(body.matches(payload).count() == 1); + } + + #[test] + fn render_custom_error_message() { + let payload = "dBgIw8DnNHxJojiXzu445qUC4UpxwZCy"; + let (body, status) = render( + "ObH9jYUl4wMhUNcXnuqwVVzHoqx4ufyN", + &tera::Context::new(), + Some(payload.to_string()), + ); + assert_eq!(status, 500); + assert!(body.matches(payload).count() == 1); + } + + #[test] + fn render_empty() { + let (body, status) = render( + "R8D1pxwHZDxcH5SMjR7rZEnIzmpkiHkH", + &tera::Context::new(), + None, + ); + assert_eq!(status, 500); + assert!(body.matches("Template render failed").count() == 1); + } + + #[test] + fn render_not_found() { + let payload = "OL6kb9qHe7Iwr7wFIRKUTeFhF34BRsQo"; + let (body, status) = render(payload, &tera::Context::new(), None); + + assert!(body.matches("TemplateNotFound").count() > 0); + assert!(body.matches(payload).count() > 0); + assert_eq!(status, 500); + } + + #[test] + fn render_bad_context() { + let (body, status) = render("node.html", &tera::Context::new(), None); + assert!(body.matches("Template render failed.").count() > 0); + assert_eq!(status, 500); + } + + #[test] + fn emergency_wrap_custom_message() { + let payload = "JLaTtsnd2IFukIOvqFNymeuiaS6nMaUc"; + let error = tera::Error::msg(payload); + let html = emergency_wrap(&error); + assert!(html.matches(payload).count() == 1); + } +} diff --git a/src/syntax/command.rs b/src/syntax/command.rs index 0c55e4e..2ca7b01 100644 --- a/src/syntax/command.rs +++ b/src/syntax/command.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use crate::prelude::*; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Arguments { pub hostname: String, pub port: u16, @@ -57,3 +57,58 @@ fn parse(defaults: &Arguments, args: &[String]) -> Arguments { } out_args } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn address() { + let args = Arguments { + hostname: String::from("localhost"), + port: 3007, + graph_path: PathBuf::new(), + }; + + assert_eq!(args.make_address(), "localhost:3007"); + } + + #[test] + fn hostname() { + let defaults = Arguments::new(); + + let payload = String::from("olUCu7vWcUAsumv2xpj2Z55EDheWLTEu"); + let args = + parse(&defaults, &[String::from("-h"), String::from(&payload)]); + assert_eq!(args.hostname, payload); + } + + #[test] + fn port() { + let defaults = Arguments::new(); + + let payload = 3901; + let args = parse(&defaults, &[String::from("-p"), payload.to_string()]); + assert_eq!(args.port, payload); + } + + #[test] + fn graph_path() { + let defaults = Arguments::new(); + + let payload = PathBuf::from("/tmp/"); + let args = parse( + &defaults, + &[String::from("-g"), payload.to_str().unwrap().to_string()], + ); + assert_eq!(args.graph_path, payload); + } + + #[test] + fn empty() { + let defaults = Arguments::new(); + + let args = parse(&defaults, &[]); + assert_eq!(defaults, args); + } +} diff --git a/src/syntax/serial.rs b/src/syntax/serial.rs index 5fb87cf..7ed2a87 100644 --- a/src/syntax/serial.rs +++ b/src/syntax/serial.rs @@ -128,11 +128,97 @@ pub fn deserialize_graph(in_format: &Format, serial: &str) -> Graph { match *in_format { Format::TOML => match toml::from_str(serial) { Ok(g) => g, - Err(error) => Graph::new(Some(error.to_string())), + Err(error) => Graph::new(Some(&error.to_string())), }, Format::JSON => match serde_json::from_str(serial) { Ok(g) => g, - Err(error) => Graph::new(Some(error.to_string())), + Err(error) => Graph::new(Some(&error.to_string())), }, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn good_json() { + let json = r#" + { + "nodes": { + "JSON": { + "text": "", + "title": "JSON", + "links": [], + "id": "JSON", + "hidden": false, + "connections": [] + } + }, + "root_node": "JSON" + } + "#; + + let graph = deserialize_graph(&Format::JSON, json); + assert!(graph.meta.messages.is_empty()); + } + + #[test] + fn bad_json() { + let graph = deserialize_graph(&Format::JSON, ":::"); + let message = graph.meta.messages.first().unwrap(); + assert!(message.contains("expected value at line 1 column 1")); + } + + #[test] + fn detached_node() { + let node = Node { + id: String::from("SomeNode"), + text: String::new(), + title: String::new(), + links: vec![String::new()], + hidden: false, + connections: Some(vec![Edge { + anchor: String::from("SomeAnchor"), + from: String::new(), + to: String::new(), + detached: false, + }]), + }; + + let mut map: HashMap = HashMap::new(); + map.insert(String::from("SomeNode"), node); + + let modulated_map = modulate_nodes(&map); + let modulated_node = modulated_map.get("SomeNode").unwrap().clone(); + let modulated_connections = modulated_node.connections.unwrap(); + let modulated_connection = modulated_connections.first().unwrap(); + assert!(modulated_connection.anchor == "SomeAnchor"); + assert!(modulated_connection.detached); + } +} + +#[cfg(test)] +mod serial_tests { + use super::*; + + #[test] + fn bad_graph_path() { + println!("T"); + let original_working_directory = std::env::current_dir().unwrap(); + + assert!( + std::env::set_current_dir(std::path::Path::new( + "tests/mocks/no_graph" + )) + .is_ok() + ); + + let graph = populate_graph(); + let message = graph.meta.messages.first().unwrap(); + assert!(message.contains("TOML parse error")); + assert!(message.contains("No such file or directory")); + + assert!(std::env::set_current_dir(original_working_directory).is_ok()); + } +} diff --git a/src/types.rs b/src/types.rs index 809d78d..00d78e6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -198,3 +198,84 @@ impl Config { } } } + +#[cfg(test)] +mod tests { + use crate::syntax::serial::populate_graph; + + use super::*; + + #[test] + fn empty_graph() { + let graph = Graph::new(Some("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj")); + assert!(graph.nodes.is_empty()); + assert!(graph.incoming.is_empty()); + assert_eq!( + graph.meta.messages.first().unwrap(), + "ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj" + ); + } + + #[test] + fn empty_node_message() { + let node = Node::new(None); + assert_eq!(node.text, "Node is empty, missing or wasn't found."); + } + + #[test] + fn empty_footer_text() { + let default_graph = populate_graph(); + + let config = Config { + footer_text: String::new(), + ..default_graph.meta.config + }; + + let parsed_config = config.parse_text(); + + println!("{:?}", parsed_config.footer_text); + assert!(parsed_config.footer_text.is_empty()); + } + + #[test] + fn config_footer_text() { + let payload = "0kqBrdS8NPrU4xVxh2xW0hUzAw926JCQ"; + let default_graph = populate_graph(); + + let config = Config { + footer_text: format!("`{payload}`"), + ..default_graph.meta.config + }; + + let parsed_config = config.parse_text(); + + assert!( + parsed_config + .footer_text + .matches(format!("{payload}").as_str()) + .count() + == 1 + ); + } + + #[test] + fn config_about_text() { + let payload = "ZqPFl84JlzSS0QUo61RwTUPONIE78Lmw"; + let default_graph = populate_graph(); + + let config = Config { + about_text: format!("`{payload}`"), + ..default_graph.meta.config + }; + + let parsed_config = config.parse_text(); + + assert!( + parsed_config + .about_text + .matches(format!("{payload}").as_str()) + .count() + == 1 + ); + } +} diff --git a/tests/mocks/bad_graph/static/graph.toml b/tests/mocks/bad_graph/static/graph.toml new file mode 100644 index 0000000..2d79863 --- /dev/null +++ b/tests/mocks/bad_graph/static/graph.toml @@ -0,0 +1,3 @@ +[meta] +config = { +' diff --git a/tests/mocks/good_json/graph.json b/tests/mocks/good_json/graph.json new file mode 100644 index 0000000..31363fb --- /dev/null +++ b/tests/mocks/good_json/graph.json @@ -0,0 +1,13 @@ +{ + "nodes": { + "JSON": { + "text": "", + "title": "JSON", + "links": [], + "id": "JSON", + "hidden": false, + "connections": [] + } + }, + "root_node": "JSON" +}