Skip to main content

max / makenotwork

6.5 KB · 185 lines History Blame Raw
1 //! Personal annotations on the browse surface: what the author sees, what
2 //! everybody else sees, and what happens to an annotation whose commit stops
3 //! being served.
4 //!
5 //! The three assertions here are the feature's done condition. The middle one
6 //! is the load-bearing one: an annotation is private writing, and a second
7 //! account or an anonymous request finding a word of it on a shared page would
8 //! be the whole feature failing rather than a bug in it.
9
10 use crate::harness::{BuildOptions, TestHarness};
11
12 /// A bare repo at `{dir}/testowner/testrepo.git` with one commit on `main`.
13 fn make_repo(dir: &std::path::Path) {
14 use crate::harness::gitfixture::{blob, commit, init_bare, tree};
15 use gix::objs::tree::EntryKind;
16
17 let repo = init_bare(dir, "testowner", "testrepo");
18 let readme = blob(&repo, b"# Test Repo\n");
19 let root = tree(&repo, &[("README.md", readme, EntryKind::Blob)]);
20 commit(&repo, "Initial commit", root, Vec::new());
21 }
22
23 /// The repository registered and public, and nobody logged in.
24 async fn setup(tmp: &tempfile::TempDir) -> (TestHarness, String) {
25 make_repo(tmp.path());
26 let mut h = TestHarness::build(BuildOptions {
27 git_repos_path: Some(tmp.path().to_str().unwrap().to_string()),
28 build_trigger_token: Some("test-trigger-secret".to_string()),
29 ..Default::default()
30 })
31 .await;
32 h.signup("testowner", "testowner@example.com", "password123")
33 .await;
34 // The first browse auto-registers the on-disk repo, and only the owner may
35 // trigger it. Public, so a stranger reaches the commit page at all.
36 h.client.get("/git/testowner/testrepo").await;
37 sqlx::query("UPDATE git_repos SET visibility = 'public' WHERE name = 'testrepo'")
38 .execute(&h.db)
39 .await
40 .unwrap();
41 h.client.post_form("/logout", "").await;
42
43 let sha = crate::harness::gitfixture::main_tip_sha(tmp.path(), "testowner", "testrepo");
44 (h, sha)
45 }
46
47 const ANNOTATION: &str = "the commit that broke the importer";
48
49 /// Sign up `who` and annotate `sha` in `testowner/testrepo`. Leaves them in.
50 async fn annotate(h: &mut TestHarness, who: &str, sha: &str, body: &str) {
51 h.signup(who, &format!("{who}@example.com"), "password123")
52 .await;
53 let resp = h
54 .client
55 .post_form(
56 &format!("/git/testowner/testrepo/commit/{sha}/annotate"),
57 &format!("content={}", urlencoding::encode(body)),
58 )
59 .await;
60 assert_eq!(resp.status, 303, "{}", resp.text);
61 }
62
63 #[tokio::test]
64 async fn a_reader_sees_their_own_annotation_on_the_commit_page() {
65 let tmp = tempfile::TempDir::new().unwrap();
66 let (mut h, sha) = setup(&tmp).await;
67 annotate(&mut h, "annotator", &sha, ANNOTATION).await;
68
69 let page = h
70 .client
71 .get(&format!("/git/testowner/testrepo/commit/{sha}"))
72 .await;
73 assert_eq!(page.status, 200);
74 assert!(
75 page.text.contains("id=\"annotations\""),
76 "the private section is missing from the author's own view"
77 );
78 assert!(
79 page.text.contains(ANNOTATION),
80 "the author cannot read what they wrote"
81 );
82 // Rendered, not only reloaded into the edit box: the section is the
83 // feature, and a page carrying it in a textarea alone would pass a naive
84 // substring check while showing nothing.
85 let body_at = page
86 .text
87 .find("id=\"annotations\"")
88 .expect("the section is present");
89 let form_at = page
90 .text
91 .find("Save annotation")
92 .expect("the edit form is present");
93 assert!(
94 page.text[body_at..form_at].contains(ANNOTATION),
95 "the annotation is not rendered above the form"
96 );
97 }
98
99 #[tokio::test]
100 async fn nobody_else_sees_it_on_the_same_page() {
101 let tmp = tempfile::TempDir::new().unwrap();
102 let (mut h, sha) = setup(&tmp).await;
103 annotate(&mut h, "annotator", &sha, ANNOTATION).await;
104 h.client.post_form("/logout", "").await;
105
106 // A second account in good standing, reading the same public commit.
107 h.signup("stranger", "stranger@example.com", "password123")
108 .await;
109 let theirs = h
110 .client
111 .get(&format!("/git/testowner/testrepo/commit/{sha}"))
112 .await;
113 assert_eq!(theirs.status, 200);
114 assert!(
115 !theirs.text.contains(ANNOTATION),
116 "another account was served somebody's private annotation"
117 );
118 // Their own empty section is offered, since they may write one; what must
119 // never appear is anybody else's row in it.
120 assert!(
121 !theirs.text.contains("git-annotation-entry"),
122 "another account was served an annotation row that is not theirs"
123 );
124 h.client.post_form("/logout", "").await;
125
126 let anon = h
127 .client
128 .get(&format!("/git/testowner/testrepo/commit/{sha}"))
129 .await;
130 assert_eq!(anon.status, 200);
131 assert!(
132 !anon.text.contains(ANNOTATION),
133 "an anonymous request was served a private annotation"
134 );
135 assert!(
136 !anon.text.contains("id=\"annotations\""),
137 "the private section is rendered for a viewer who has no account"
138 );
139 }
140
141 #[tokio::test]
142 async fn an_annotation_outlives_the_repository_it_was_written_against() {
143 let tmp = tempfile::TempDir::new().unwrap();
144 let (mut h, sha) = setup(&tmp).await;
145 annotate(&mut h, "annotator", &sha, ANNOTATION).await;
146
147 // The repository goes away. Nothing touches the annotation, which lives in
148 // the reader's own repository and is theirs.
149 sqlx::query("DELETE FROM git_repos WHERE name = 'testrepo'")
150 .execute(&h.db)
151 .await
152 .unwrap();
153
154 let listing = h.client.get("/git/my-annotations").await;
155 assert_eq!(listing.status, 200, "{}", listing.text);
156 assert!(
157 listing.text.contains(ANNOTATION),
158 "the annotation was dropped with the repository it annotated"
159 );
160 assert!(
161 listing.text.contains("no longer serves"),
162 "an orphan is listed without being marked as one"
163 );
164 assert!(
165 listing.text.contains("testowner/testrepo"),
166 "the origin is still named even though it no longer answers"
167 );
168
169 // Nothing collected it: the row is still in the index, and the note is
170 // still in the repository the index projects.
171 let annotator = sqlx::query_scalar::<_, makenotwork::db::UserId>(
172 "SELECT id FROM users WHERE username = 'annotator'",
173 )
174 .fetch_one(&h.db)
175 .await
176 .unwrap();
177 assert_eq!(
178 makenotwork::db::git_notes::count_user_annotations(&h.db, annotator)
179 .await
180 .unwrap(),
181 1,
182 "an orphan was collected, and nothing is allowed to collect one"
183 );
184 }
185