//! Personal annotations on the browse surface: what the author sees, what //! everybody else sees, and what happens to an annotation whose commit stops //! being served. //! //! The three assertions here are the feature's done condition. The middle one //! is the load-bearing one: an annotation is private writing, and a second //! account or an anonymous request finding a word of it on a shared page would //! be the whole feature failing rather than a bug in it. use crate::harness::{BuildOptions, TestHarness}; /// A bare repo at `{dir}/testowner/testrepo.git` with one commit on `main`. fn make_repo(dir: &std::path::Path) { use crate::harness::gitfixture::{blob, commit, init_bare, tree}; use gix::objs::tree::EntryKind; let repo = init_bare(dir, "testowner", "testrepo"); let readme = blob(&repo, b"# Test Repo\n"); let root = tree(&repo, &[("README.md", readme, EntryKind::Blob)]); commit(&repo, "Initial commit", root, Vec::new()); } /// The repository registered and public, and nobody logged in. async fn setup(tmp: &tempfile::TempDir) -> (TestHarness, String) { make_repo(tmp.path()); let mut h = TestHarness::build(BuildOptions { git_repos_path: Some(tmp.path().to_str().unwrap().to_string()), build_trigger_token: Some("test-trigger-secret".to_string()), ..Default::default() }) .await; h.signup("testowner", "testowner@example.com", "password123") .await; // The first browse auto-registers the on-disk repo, and only the owner may // trigger it. Public, so a stranger reaches the commit page at all. h.client.get("/git/testowner/testrepo").await; sqlx::query("UPDATE git_repos SET visibility = 'public' WHERE name = 'testrepo'") .execute(&h.db) .await .unwrap(); h.client.post_form("/logout", "").await; let sha = crate::harness::gitfixture::main_tip_sha(tmp.path(), "testowner", "testrepo"); (h, sha) } const ANNOTATION: &str = "the commit that broke the importer"; /// Sign up `who` and annotate `sha` in `testowner/testrepo`. Leaves them in. async fn annotate(h: &mut TestHarness, who: &str, sha: &str, body: &str) { h.signup(who, &format!("{who}@example.com"), "password123") .await; let resp = h .client .post_form( &format!("/git/testowner/testrepo/commit/{sha}/annotate"), &format!("content={}", urlencoding::encode(body)), ) .await; assert_eq!(resp.status, 303, "{}", resp.text); } #[tokio::test] async fn a_reader_sees_their_own_annotation_on_the_commit_page() { let tmp = tempfile::TempDir::new().unwrap(); let (mut h, sha) = setup(&tmp).await; annotate(&mut h, "annotator", &sha, ANNOTATION).await; let page = h .client .get(&format!("/git/testowner/testrepo/commit/{sha}")) .await; assert_eq!(page.status, 200); assert!( page.text.contains("id=\"annotations\""), "the private section is missing from the author's own view" ); assert!( page.text.contains(ANNOTATION), "the author cannot read what they wrote" ); // Rendered, not only reloaded into the edit box: the section is the // feature, and a page carrying it in a textarea alone would pass a naive // substring check while showing nothing. let body_at = page .text .find("id=\"annotations\"") .expect("the section is present"); let form_at = page .text .find("Save annotation") .expect("the edit form is present"); assert!( page.text[body_at..form_at].contains(ANNOTATION), "the annotation is not rendered above the form" ); } #[tokio::test] async fn nobody_else_sees_it_on_the_same_page() { let tmp = tempfile::TempDir::new().unwrap(); let (mut h, sha) = setup(&tmp).await; annotate(&mut h, "annotator", &sha, ANNOTATION).await; h.client.post_form("/logout", "").await; // A second account in good standing, reading the same public commit. h.signup("stranger", "stranger@example.com", "password123") .await; let theirs = h .client .get(&format!("/git/testowner/testrepo/commit/{sha}")) .await; assert_eq!(theirs.status, 200); assert!( !theirs.text.contains(ANNOTATION), "another account was served somebody's private annotation" ); // Their own empty section is offered, since they may write one; what must // never appear is anybody else's row in it. assert!( !theirs.text.contains("git-annotation-entry"), "another account was served an annotation row that is not theirs" ); h.client.post_form("/logout", "").await; let anon = h .client .get(&format!("/git/testowner/testrepo/commit/{sha}")) .await; assert_eq!(anon.status, 200); assert!( !anon.text.contains(ANNOTATION), "an anonymous request was served a private annotation" ); assert!( !anon.text.contains("id=\"annotations\""), "the private section is rendered for a viewer who has no account" ); } #[tokio::test] async fn an_annotation_outlives_the_repository_it_was_written_against() { let tmp = tempfile::TempDir::new().unwrap(); let (mut h, sha) = setup(&tmp).await; annotate(&mut h, "annotator", &sha, ANNOTATION).await; // The repository goes away. Nothing touches the annotation, which lives in // the reader's own repository and is theirs. sqlx::query("DELETE FROM git_repos WHERE name = 'testrepo'") .execute(&h.db) .await .unwrap(); let listing = h.client.get("/git/my-annotations").await; assert_eq!(listing.status, 200, "{}", listing.text); assert!( listing.text.contains(ANNOTATION), "the annotation was dropped with the repository it annotated" ); assert!( listing.text.contains("no longer serves"), "an orphan is listed without being marked as one" ); assert!( listing.text.contains("testowner/testrepo"), "the origin is still named even though it no longer answers" ); // Nothing collected it: the row is still in the index, and the note is // still in the repository the index projects. let annotator = sqlx::query_scalar::<_, makenotwork::db::UserId>( "SELECT id FROM users WHERE username = 'annotator'", ) .fetch_one(&h.db) .await .unwrap(); assert_eq!( makenotwork::db::git_notes::count_user_annotations(&h.db, annotator) .await .unwrap(), 1, "an orphan was collected, and nothing is allowed to collect one" ); }