1use super::*; 2 3const LISTING: &str = r#"[ 4 {"name":"README.md","path":"third-party/README.md","type":"file","size":335,"sha":"de91","download_url":"https://raw.githubusercontent.com/deizel/lmjtfy/main/third-party/README.md?token=SECRET"}, 5 {"name":"jevcrates","path":"third-party/jevcrates","type":"file","size":0,"sha":"33ec7cc","download_url":null}, 6 {"name":"apps","path":"apps","type":"dir","size":0,"sha":"aa","download_url":null} 7]"#; 8 9#[test] 10fn a_listing_puts_folders_first_and_tells_a_submodule_from_a_file() { 11 let Some(Contents::Dir(entries)) = parse(LISTING) else { panic!("a listing") }; 12 let kinds: Vec<_> = entries.iter().map(|entry| (entry.name.as_str(), entry.kind)).collect(); 13 assert_eq!(kinds, [("apps", Kind::Dir), ("jevcrates", Kind::Submodule), ("README.md", Kind::File)]); 14} 15 16#[test] 17fn the_download_token_is_never_kept() { 18 let parsed = format!("{:?}", parse(LISTING)); 19 assert!(!parsed.contains("SECRET") && !parsed.contains("token"), "{parsed}"); 20} 21 22#[test] 23fn a_file_is_decoded_and_a_binary_one_is_not_shown() { 24 let file = r#"{"name":"a.rs","path":"src/a.rs","type":"file","size":6,"download_url":"x","encoding":"base64","content":"Zm4g\nbWFpbg==\n"}"#; 25 assert_eq!(parse(file), Some(Contents::File { path: "src/a.rs".into(), size: 6, body: Body::Text("fn main".into()) })); 26 let png = r#"{"name":"a.png","path":"a.png","type":"file","size":3,"download_url":"x","encoding":"base64","content":"iVAA"}"#; 27 assert!(matches!(parse(png), Some(Contents::File { body: Body::Binary(_), .. }))); 28 assert_eq!(media_type("a.PNG", &Body::Binary(vec![1])), "image/png"); 29 assert_eq!(media_type("index.html", &Body::Text(String::new())), "text/plain; charset=utf-8"); 30 let huge = r#"{"name":"b","path":"b","type":"file","size":5000000,"download_url":"x","encoding":"none","content":""}"#; 31 assert!(matches!(parse(huge), Some(Contents::File { body: Body::TooLarge, .. }))); 32} 33 34#[test] 35fn a_submodule_asked_for_gives_its_pinned_commit() { 36 let json = r#"{"type":"submodule","sha":"33ec7cca","submodule_git_url":"../jevcrates.git","name":"jevcrates","path":"third-party/jevcrates"}"#; 37 assert_eq!(parse(json), Some(Contents::Submodule { path: "third-party/jevcrates".into(), sha: "33ec7cca".into() })); 38} 39 40#[test] 41fn a_path_cannot_climb_out() { 42 assert_eq!(segments("packages//rules/src/"), Some(vec!["packages", "rules", "src"])); 43 assert_eq!(segments(""), Some(vec![])); 44 assert_eq!(segments("packages/../.."), None); 45 assert_eq!(segments("./a"), None); 46 assert_eq!(encoded(&["a b", "c?.rs"]), "a%20b/c%3F.rs"); 47} 48 49#[test] 50fn relative_links_stay_in_the_code_pages() { 51 let base = "/lmjtfy.git"; 52 assert_eq!(link("apps/lmjtfy/README.md", "", base), "/lmjtfy.git/apps/lmjtfy/README.md"); 53 assert_eq!(link("../README.md#build", "packages/rules", base), "/lmjtfy.git/packages/README.md#build"); 54 assert_eq!(link("src/", "packages/rules", base), "/lmjtfy.git/packages/rules/src/"); 55 assert_eq!(link("https://pitchfork.jdx.dev", "", base), "https://pitchfork.jdx.dev"); 56 assert_eq!(link("#stack", "", base), "#stack"); 57 assert_eq!(parent("packages/rules/README.md"), "packages/rules"); 58 assert_eq!(parent("README.md"), ""); 59} 60 61#[test] 62fn markdown_renders_with_its_links_rewritten_and_no_raw_html() { 63 let html = markdown("# Hi\n\n[rules](packages/rules)\n\n<script>alert(1)</script>\n\n| a | b |\n|---|---|\n| 1 | 2 |\n", "", "/lmjtfy.git").html; 64 assert!(html.contains(r#"<a href="/lmjtfy.git/packages/rules">rules</a>"#), "{html}"); 65 assert!(!html.contains("<script>"), "{html}"); 66 assert!(html.contains("<script>"), "{html}"); 67 assert!(html.contains("<table>"), "{html}"); 68} 69 70#[test] 71fn headings_get_unique_anchors_for_the_contents() { 72 let rendered = markdown("# lmjtfy\n\n## How it works\n\n### `Network::next`\n\n## How it works\n", "", "/lmjtfy.git"); 73 let ids: Vec<_> = rendered.headings.iter().map(|heading| (heading.level, heading.id.as_str())).collect(); 74 assert_eq!(ids, [(1, "lmjtfy"), (2, "how-it-works"), (3, "network-next"), (2, "how-it-works-1")]); 75 assert!(rendered.html.contains(r#"<h2 id="how-it-works">How it works</h2>"#), "{}", rendered.html); 76 assert!(!rendered.mermaid); 77} 78 79#[test] 80fn a_mermaid_block_is_left_for_the_page_to_draw_and_stays_escaped() { 81 let rendered = markdown("```mermaid\nflowchart LR\n a[<b>x</b>] --> b\n```\n", "", "/lmjtfy.git"); 82 assert!(rendered.mermaid); 83 assert!(rendered.html.contains(r#"<pre class="mermaid">flowchart LR"#), "{}", rendered.html); 84 assert!(rendered.html.contains("<b>x</b>"), "{}", rendered.html); 85 assert!(!rendered.html.contains("<code"), "{}", rendered.html); 86} 87 88#[test] 89fn a_claude_md_shows_its_import_as_a_link() { 90 assert_eq!(agents("@README.md\n\n- **A rule.**\n"), (Some("README.md"), "\n- **A rule.**\n")); 91 assert_eq!(agents("# No import\n"), (None, "# No import\n")); 92} 93 94#[test] 95fn a_whole_tree_is_read_with_its_submodules_pins_and_mounts_under_them() { 96 let json = r#"{"sha":"x","truncated":false,"tree":[ 97 {"path":"README.md","mode":"100644","type":"blob","sha":"a","size":10,"url":"u"}, 98 {"path":"packages","mode":"040000","type":"tree","sha":"b","url":"u"}, 99 {"path":"packages/rules","mode":"040000","type":"tree","sha":"c","url":"u"}, 100 {"path":"packages/rules/Cargo.toml","mode":"100644","type":"blob","sha":"d","size":5,"url":"u"}, 101 {"path":"third-party/jevcrates","mode":"160000","type":"commit","sha":"4651441"} 102 ]}"#; 103 let mut tree = parse_tree(json).unwrap(); 104 assert_eq!(tree.pins, [("third-party/jevcrates".to_owned(), "4651441".to_owned())]); 105 let root: Vec<_> = tree.children("").iter().map(|entry| entry.name.as_str()).collect(); 106 assert_eq!(root, ["packages", "README.md"]); 107 assert_eq!(tree.children("packages")[0].kind, Kind::Dir); 108 let inner = parse_tree(r#"{"truncated":false,"tree":[{"path":"jev-client","type":"tree","sha":"e"}]}"#).unwrap(); 109 tree.mount("third-party/jevcrates", inner); 110 assert_eq!(tree.children("third-party/jevcrates")[0].path, "third-party/jevcrates/jev-client"); 111 assert_eq!(parse_tree(r#"{"truncated":true,"tree":[]}"#), None); 112} 113 114#[test] 115fn an_image_is_drawn_from_the_file_and_not_its_page() { 116 let rendered = markdown("  ", "docs", "/postjevsql.git"); 117 assert!(rendered.html.contains(r#"src="/postjevsql.git/docs/postjevsql.png?raw""#), "{}", rendered.html); 118 assert!(rendered.html.contains(r#"src="/postjevsql.git/x.svg?raw""#), "{}", rendered.html); 119 assert!(rendered.html.contains(r#"src="https://e.com/c.png""#), "{}", rendered.html); 120 // A link to the same file still goes to its page. 121 assert!(markdown("[a](postjevsql.png)", "docs", "/p.git").html.contains(r#"href="/p.git/docs/postjevsql.png""#)); 122} 123 124#[test] 125fn a_chapter_says_where_it_sits_in_its_guide() { 126 let text = "# Chapter 6\n\nBody with Next: in prose.\n\n← Previous: [Chapter 5, packages/](../) · Up: [packages](../) · Next: [Chapter 6½, src/](src/) →\n"; 127 let found = guide(text, "packages/rules", "/lmjtfy.git"); 128 assert!(found.line); 129 assert_eq!(found.previous, Some(("Chapter 5, packages/".into(), "/lmjtfy.git/packages/".into()))); 130 assert_eq!(found.next, Some(("Chapter 6½, src/".into(), "/lmjtfy.git/packages/rules/src/".into()))); 131 // A prologue has only a Next; a previous with no link is not there. 132 let prologue = guide("# x\n\nNext: [Chapter 1, apps/](apps/) →", "", "/lmjtfy.git"); 133 assert_eq!(prologue.next, Some(("Chapter 1, apps/".into(), "/lmjtfy.git/apps/".into()))); 134 assert_eq!(prologue.previous, None); 135 let unlinked = guide("← Previous: lmjtfy's chapter · Next: [Chapter 17](jev-protocol/) →", "", "/jevcrates.git"); 136 assert_eq!(unlinked.previous, None); 137 assert!(unlinked.next.is_some()); 138 assert_eq!(guide("# no guide line", "", "/x.git"), Guide::default()); 139}