// SPDX-FileCopyrightText: 2026 Ololduck <ololduck@vit.am> and contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
//! Integration tests for note editing.
mod support;
use support::{Client, Server, free_port};
/// The password the tests register and log in with.
const PASSWORD: &str = "correct horse battery staple";
/// Registers `name` through the form, which logs them in.
fn register(client: &mut Client, name: &str) {
let email = format!("{name}@example.org");
let response = client.post_form(
"/register",
&[("name", name), ("email", &email), ("password", PASSWORD)],
);
assert_eq!(
response.status, 303,
"registering should log in\n{}",
response.body
);
}
/// Test: Author can access the edit form for their own note.
#[tokio::test]
async fn author_can_access_edit_form() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let mut client = Client::new(port);
register(&mut client, "alice");
// Create a note
let response = client.post_multipart(
"/~alice/notes",
&[("content", "Original note content")],
&[],
);
assert_eq!(response.status, 303, "creating note should redirect");
// Extract the note UUID from the redirect location or profile page
let profile = client.get("/~alice");
assert!(
profile.body.contains("Original note content"),
"note should appear on profile"
);
// Find the note UUID from the profile page
let note_uuid = extract_note_uuid(&profile.body);
assert!(note_uuid.is_some(), "should find note UUID on profile");
// Access the edit form
let edit_page = client.get(&format!("/~alice/notes/{}/edit", note_uuid.unwrap()));
assert_eq!(
edit_page.status, 200,
"author should access edit form\n{}",
edit_page.body
);
assert!(
edit_page.body.contains("Edit Note"),
"edit form should have title"
);
assert!(
edit_page.body.contains("Original note content"),
"edit form should pre-fill content"
);
Ok(())
}
/// Test: Non-author cannot access the edit form.
#[tokio::test]
async fn non_author_cannot_access_edit_form() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
// Alice creates a note
let mut alice_client = Client::new(port);
register(&mut alice_client, "alice");
let response =
alice_client.post_multipart("/~alice/notes", &[("content", "Alice's note")], &[]);
assert_eq!(response.status, 303);
let profile = alice_client.get("/~alice");
let note_uuid = extract_note_uuid(&profile.body).unwrap();
// Bob tries to edit Alice's note
let mut bob_client = Client::new(port);
register(&mut bob_client, "bob");
let edit_page = bob_client.get(&format!("/~alice/notes/{}/edit", note_uuid));
assert_eq!(
edit_page.status, 403,
"non-author should get 403\n{}",
edit_page.body
);
Ok(())
}
/// Test: Anonymous user cannot access the edit form.
#[tokio::test]
async fn anonymous_cannot_access_edit_form() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
// Alice creates a note
let mut alice_client = Client::new(port);
register(&mut alice_client, "alice");
alice_client.post_multipart("/~alice/notes", &[("content", "Alice's note")], &[]);
let profile = alice_client.get("/~alice");
let note_uuid = extract_note_uuid(&profile.body).unwrap();
// Anonymous user tries to edit
let mut anon_client = Client::new(port);
let edit_page = anon_client.get(&format!("/~alice/notes/{}/edit", note_uuid));
assert_eq!(
edit_page.status, 401,
"anonymous should get 401\n{}",
edit_page.body
);
Ok(())
}
/// Test: Author can edit their note.
#[tokio::test]
async fn author_can_edit_note() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let mut client = Client::new(port);
register(&mut client, "alice");
// Create a note
client.post_multipart("/~alice/notes", &[("content", "Original content")], &[]);
let profile = client.get("/~alice");
let note_uuid = extract_note_uuid(&profile.body).unwrap();
// Edit the note
let response = client.post_multipart(
&format!("/~alice/notes/{}/edit", note_uuid),
&[("content", "Updated content")],
&[],
);
assert_eq!(
response.status, 303,
"editing should redirect\n{}",
response.body
);
// Verify the update
let note_page = client.get(&format!("/~alice/notes/{}", note_uuid));
assert_eq!(note_page.status, 200);
assert!(
note_page.body.contains("Updated content"),
"note should show updated content\n{}",
note_page.body
);
assert!(
note_page.body.contains("(edited)"),
"note should show edited indicator\n{}",
note_page.body
);
Ok(())
}
/// Test: Non-author cannot edit a note.
#[tokio::test]
async fn non_author_cannot_edit_note() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
// Alice creates a note
let mut alice_client = Client::new(port);
register(&mut alice_client, "alice");
alice_client.post_multipart("/~alice/notes", &[("content", "Alice's note")], &[]);
let profile = alice_client.get("/~alice");
let note_uuid = extract_note_uuid(&profile.body).unwrap();
// Bob tries to edit
let mut bob_client = Client::new(port);
register(&mut bob_client, "bob");
let response = bob_client.post_form(
&format!("/~alice/notes/{}/edit", note_uuid),
&[("content", "Bob's edit")],
);
assert_eq!(
response.status, 403,
"non-author should get 403\n{}",
response.body
);
Ok(())
}
/// Test: Edit form shows edit button only to author.
#[tokio::test]
async fn edit_button_visible_to_author_only() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
// Alice creates a note
let mut alice_client = Client::new(port);
register(&mut alice_client, "alice");
alice_client.post_multipart("/~alice/notes", &[("content", "Alice's note")], &[]);
let profile = alice_client.get("/~alice");
let note_uuid = extract_note_uuid(&profile.body).unwrap();
// Alice sees edit button
let note_page = alice_client.get(&format!("/~alice/notes/{}", note_uuid));
assert!(
note_page.body.contains("/edit"),
"author should see edit link\n{}",
note_page.body
);
// Bob doesn't see edit button
let mut bob_client = Client::new(port);
register(&mut bob_client, "bob");
let bob_note_page = bob_client.get(&format!("/~alice/notes/{}", note_uuid));
assert!(
!bob_note_page.body.contains("Edit Note"),
"non-author should not see edit button\n{}",
bob_note_page.body
);
Ok(())
}
/// Helper to extract the first note UUID from a profile page.
fn extract_note_uuid(html: &str) -> Option<String> {
// Look for note links in the format href="/~username/notes/{uuid}"
let pattern = "href=\"/~";
if let Some(start) = html.find(pattern) {
let after_href = &html[start + pattern.len()..];
// Find /notes/ in the remaining string
if let Some(notes_pos) = after_href.find("/notes/") {
let uuid_start = notes_pos + "/notes/".len();
let uuid_part = &after_href[uuid_start..];
// UUID is 36 characters, extract it
if uuid_part.len() >= 36 {
let uuid = &uuid_part[..36];
// Verify it looks like a UUID (contains hyphens)
if uuid.contains('-') {
return Some(uuid.to_string());
}
}
}
}
None
}