//! Integration tests for the patch proposals web UI.
//!
//! These tests verify the list, detail, and edit pages for patch proposals,
//! including visibility rules and permissions.
mod support;
use sea_orm::{ActiveModelTrait, ColumnTrait, Database, EntityTrait, QueryFilter, Set};
use support::{Client, Server, free_port};
use uuid::Uuid;
const PASSWORD: &str = "correct horse battery staple";
fn spawn() -> (Server, u16) {
let port = free_port();
let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
(server, port)
}
/// 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
);
}
/// Creates a repository through the form.
fn create_repository(client: &mut Client, owner: &str, slug: &str, visibility: &str) {
let response = client.post_form(
&format!("/~{owner}/repositories"),
&[
("slug", slug),
("description", "a repository from the patch tests"),
("visibility", visibility),
],
);
assert_eq!(
response.status, 303,
"creating a repository should redirect\n{}",
response.body
);
}
/// Creates a patch proposal directly in the database for testing.
async fn create_patch(
server: &Server,
repository_id: i64,
creator_id: i64,
number: i64,
summary: &str,
status: &str,
) -> i64 {
use chrono::Utc;
use frogue::entities::patch_proposals::{ActiveModel, MergePolicy, Status};
let db = Database::connect(server.database_url())
.await
.expect("connecting to test database");
let status = match status {
"draft" => Status::Draft,
"open" => Status::Open,
"closed" => Status::Closed,
"merged" => Status::Merged,
_ => panic!("unknown status"),
};
let now = Utc::now().fixed_offset();
let pp = ActiveModel {
uuid: Set(Uuid::new_v4()),
repository_id: Set(repository_id),
creator_id: Set(Some(creator_id)),
number: Set(number),
summary: Set(summary.to_string()),
description: Set(Some("Test description".to_string())),
source_ref: Set(format!("patches/{number}")),
target_branch: Set("main".to_string()),
status: Set(status),
merge_policy: Set(MergePolicy::PreferFastForward),
created_at: Set(now),
updated_at: Set(now),
..Default::default()
};
let pp = pp.insert(&db).await.expect("inserting patch");
pp.id
}
#[actix_web::test]
async fn the_patches_list_page_is_shown() -> miette::Result<()> {
let (_server, port) = spawn();
let mut alice = Client::new(port);
register(&mut alice, "alice");
create_repository(&mut alice, "alice", "test-repo", "public");
// Visit the patches list page
let response = alice.get("/~alice/test-repo/patches");
assert_eq!(response.status, 200, "patches list should be shown");
assert!(
response.body.contains("Patches"),
"page should have patches heading"
);
Ok(())
}
#[actix_web::test]
async fn drafts_are_only_visible_to_creator_and_owner() -> miette::Result<()> {
let (server, port) = spawn();
let mut alice = Client::new(port);
register(&mut alice, "alice");
create_repository(&mut alice, "alice", "test-repo", "public");
// Get repository and user IDs
let db = Database::connect(server.database_url())
.await
.expect("connecting to test database");
let repository = frogue::entities::repositories::Entity::find()
.filter(frogue::entities::repositories::Column::Slug.eq("test-repo"))
.one(&db)
.await
.expect("finding repository")
.expect("repository should exist");
let _alice_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("alice"))
.one(&db)
.await
.expect("finding alice")
.expect("alice should exist");
// Register bob
let mut bob = Client::new(port);
register(&mut bob, "bob");
let bob_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("bob"))
.one(&db)
.await
.expect("finding bob")
.expect("bob should exist");
// Bob creates a draft patch
create_patch(
&server,
repository.id,
bob_user.id,
1,
"Bob's draft",
"draft",
)
.await;
// Bob (creator) can see the draft
let response = bob.get(&format!("/~alice/test-repo/patches/1"));
assert_eq!(response.status, 200, "creator should see draft");
assert!(response.body.contains("draft"), "should show draft status");
// Alice (owner) can see the draft
let response = alice.get(&format!("/~alice/test-repo/patches/1"));
assert_eq!(response.status, 200, "owner should see draft");
// Register carol (stranger) who cannot see the draft
let mut carol = Client::new(port);
register(&mut carol, "carol");
let response = carol.get(&format!("/~alice/test-repo/patches/1"));
assert_eq!(
response.status, 404,
"stranger should not see draft, got: {}",
response.body
);
Ok(())
}
#[actix_web::test]
async fn open_patches_are_visible_to_everyone() -> miette::Result<()> {
let (server, port) = spawn();
let mut alice = Client::new(port);
register(&mut alice, "alice");
create_repository(&mut alice, "alice", "test-repo", "public");
// Get repository and user IDs
let db = Database::connect(server.database_url())
.await
.expect("connecting to test database");
let repository = frogue::entities::repositories::Entity::find()
.filter(frogue::entities::repositories::Column::Slug.eq("test-repo"))
.one(&db)
.await
.expect("finding repository")
.expect("repository should exist");
let alice_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("alice"))
.one(&db)
.await
.expect("finding alice")
.expect("alice should exist");
// Alice creates an open patch
create_patch(
&server,
repository.id,
alice_user.id,
1,
"Open patch",
"open",
)
.await;
// Register bob (stranger) who can see the open patch
let mut bob = Client::new(port);
register(&mut bob, "bob");
let response = bob.get(&format!("/~alice/test-repo/patches/1"));
assert_eq!(response.status, 200, "stranger should see open patch");
assert!(response.body.contains("open"), "should show open status");
Ok(())
}
#[actix_web::test]
async fn only_creator_or_owner_can_edit_patch() -> miette::Result<()> {
let (server, port) = spawn();
let mut alice = Client::new(port);
register(&mut alice, "alice");
create_repository(&mut alice, "alice", "test-repo", "public");
// Get repository and user IDs
let db = Database::connect(server.database_url())
.await
.expect("connecting to test database");
let repository = frogue::entities::repositories::Entity::find()
.filter(frogue::entities::repositories::Column::Slug.eq("test-repo"))
.one(&db)
.await
.expect("finding repository")
.expect("repository should exist");
let _alice_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("alice"))
.one(&db)
.await
.expect("finding alice")
.expect("alice should exist");
// Register bob
let mut bob = Client::new(port);
register(&mut bob, "bob");
let bob_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("bob"))
.one(&db)
.await
.expect("finding bob")
.expect("bob should exist");
// Bob creates an open patch
create_patch(
&server,
repository.id,
bob_user.id,
1,
"Bob's patch",
"open",
)
.await;
// Register carol (stranger) who cannot edit the patch
let mut carol = Client::new(port);
register(&mut carol, "carol");
let response = carol.get(&format!("/~alice/test-repo/patches/1/edit"));
assert_eq!(
response.status, 403,
"stranger should not edit patch, got: {}",
response.body
);
// Bob (creator) can edit
let response = bob.get(&format!("/~alice/test-repo/patches/1/edit"));
assert_eq!(response.status, 200, "creator should edit patch");
// Alice (owner) can edit
let response = alice.get(&format!("/~alice/test-repo/patches/1/edit"));
assert_eq!(response.status, 200, "owner should edit patch");
Ok(())
}
#[actix_web::test]
async fn status_transitions_work() -> miette::Result<()> {
let (server, port) = spawn();
let mut alice = Client::new(port);
register(&mut alice, "alice");
create_repository(&mut alice, "alice", "test-repo", "public");
// Get repository and user IDs
let db = Database::connect(server.database_url())
.await
.expect("connecting to test database");
let repository = frogue::entities::repositories::Entity::find()
.filter(frogue::entities::repositories::Column::Slug.eq("test-repo"))
.one(&db)
.await
.expect("finding repository")
.expect("repository should exist");
let alice_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("alice"))
.one(&db)
.await
.expect("finding alice")
.expect("alice should exist");
// Alice creates a draft patch
create_patch(
&server,
repository.id,
alice_user.id,
1,
"Draft patch",
"draft",
)
.await;
// Alice opens the patch via edit page
let response = alice.post_form(
&format!("/~alice/test-repo/patches/1"),
&[
("summary", "Draft patch"),
("description", "Test description"),
("target_branch", "main"),
("status", "open"),
],
);
assert_eq!(response.status, 303, "edit should redirect");
// Verify the patch is now open
let response = alice.get(&format!("/~alice/test-repo/patches/1"));
assert_eq!(response.status, 200, "patch should be shown");
assert!(response.body.contains("open"), "should show open status");
// Alice closes the patch
let response = alice.post_form(
&format!("/~alice/test-repo/patches/1"),
&[
("summary", "Draft patch"),
("description", "Test description"),
("target_branch", "main"),
("status", "closed"),
],
);
assert_eq!(response.status, 303, "edit should redirect");
// Verify the patch is now closed
let response = alice.get(&format!("/~alice/test-repo/patches/1"));
assert_eq!(response.status, 200, "patch should be shown");
assert!(
response.body.contains("closed"),
"should show closed status"
);
Ok(())
}
#[actix_web::test]
async fn only_owner_can_merge_patch() -> miette::Result<()> {
let (server, port) = spawn();
let mut alice = Client::new(port);
register(&mut alice, "alice");
create_repository(&mut alice, "alice", "test-repo", "public");
// Get repository and user IDs
let db = Database::connect(server.database_url())
.await
.expect("connecting to test database");
let repository = frogue::entities::repositories::Entity::find()
.filter(frogue::entities::repositories::Column::Slug.eq("test-repo"))
.one(&db)
.await
.expect("finding repository")
.expect("repository should exist");
let _alice_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("alice"))
.one(&db)
.await
.expect("finding alice")
.expect("alice should exist");
// Register bob
let mut bob = Client::new(port);
register(&mut bob, "bob");
let bob_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("bob"))
.one(&db)
.await
.expect("finding bob")
.expect("bob should exist");
// Bob creates an open patch
create_patch(
&server,
repository.id,
bob_user.id,
1,
"Bob's patch",
"open",
)
.await;
// Bob (creator, but not owner) cannot merge
let response = bob.post_form(&format!("/~alice/test-repo/patches/1/merge"), &[]);
assert_eq!(
response.status, 403,
"creator should not merge patch, got: {}",
response.body
);
// Register carol (stranger) who cannot merge
let mut carol = Client::new(port);
register(&mut carol, "carol");
let response = carol.post_form(&format!("/~alice/test-repo/patches/1/merge"), &[]);
assert_eq!(
response.status, 403,
"stranger should not merge patch, got: {}",
response.body
);
// Alice (owner) can merge (will fail because no actual git refs, but should be 403 for others)
// For now, just test that the endpoint exists and rejects non-owners
Ok(())
}
#[actix_web::test]
async fn owner_can_merge_a_patch() -> miette::Result<()> {
let (server, port) = spawn();
let mut alice = Client::new(port);
register(&mut alice, "alice");
create_repository(&mut alice, "alice", "test-repo", "public");
// Get repository and user IDs
let db = Database::connect(server.database_url())
.await
.expect("connecting to test database");
let repository = frogue::entities::repositories::Entity::find()
.filter(frogue::entities::repositories::Column::Slug.eq("test-repo"))
.one(&db)
.await
.expect("finding repository")
.expect("repository should exist");
let alice_user = frogue::entities::users::Entity::find()
.filter(frogue::entities::users::Column::Name.eq("alice"))
.one(&db)
.await
.expect("finding alice")
.expect("alice should exist");
// Alice creates an open patch
create_patch(
&server,
repository.id,
alice_user.id,
1,
"Alice's patch",
"open",
)
.await;
// Alice (owner) can attempt to merge (will fail because no actual git refs exist)
// but should get a redirect or error, not 403
let response = alice.post_form(&format!("/~alice/test-repo/patches/1/merge"), &[]);
// The merge will fail because there are no actual git refs, but it should not be 403
assert_ne!(
response.status, 403,
"owner should be allowed to merge, got: {}",
response.body
);
Ok(())
}