lmjtfy.git / packages / tree / src / tests.rs
tests.rsannotatedtests.rssource139 lines · 7.8 KB · raw
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("&lt;script&gt;"), "{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("&lt;b&gt;x&lt;/b&gt;"), "{}", 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("![a](postjevsql.png) ![b](../x.svg#top) ![c](https://e.com/c.png)", "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}