use rocket::figment::util::map; use rocket::figment::value::{Map, Value}; use rocket::http::{ContentType, Header, Status}; use rocket::local::asynchronous::{Client, LocalRequest}; use sqlx::mysql::{MySql, MySqlConnectOptions, MySqlPoolOptions}; use sqlx::{Acquire, Executor, Pool}; use std::fmt::Display; use std::sync::OnceLock; use stdext::function_name; use testdir::testdir; use crate::api_model; struct RealIP(&'static str); impl From<&RealIP> for Header<'static> { fn from(ip: &RealIP) -> Header<'static> { Header::new("X-Real-IP", ip.0) } } static FAKE_IP: RealIP = RealIP("127.0.1.10"); static ANOTHER_FAKE_IP: RealIP = RealIP("192.168.0.1"); static MASTER_POOL: OnceLock> = OnceLock::new(); fn find_password(url: &'_ str) -> Option<&'_ str> { let protocol = url.find("://"); if protocol.is_none() { return None; } let specific = &url[protocol.unwrap() + 3..]; let at = specific.find('@'); if at.is_none() { return None; } let auth = &specific[0..at.unwrap()]; let colon = auth.find(':'); if colon.is_none() { return None; } return Some(&auth[colon.unwrap() + 1..]); } fn make_db_name_safe(name: &str) -> String { let mut ret = String::new(); for c in name.chars() { if c >= 'a' && c <= 'z' { ret.push(c); } else if c >= '0' && c <= '9' { ret.push(c); } else { ret.push('_'); } } return ret; } async fn async_client_with_private_database(test_name: String) -> Client { let base_figment = rocket::Config::figment(); let base_url_value = base_figment .find_value("databases.eyeballs.url") .expect("database_url"); let base_url = base_url_value.as_str().expect("database_url as string"); let base_options: MySqlConnectOptions = base_url.parse().expect("valid database_url"); let maybe_password = find_password(base_url); let database = make_db_name_safe(&format!("_{}", test_name.trim_end_matches("::{{closure}}"))[..]); // Cannot get sqlx::test (0.7.4) to work with MySQL, always errors out // with connection (already?) closed when closing the setup connection. // So doing our own lazier setup where each test gets a db based on // their name. { let mut pool_conn = MASTER_POOL .get_or_init(|| { let options: MySqlConnectOptions = base_url_value.as_str().unwrap().parse().unwrap(); MySqlPoolOptions::new() .max_connections(20) .after_release(|_conn, _| Box::pin(async move { Ok(false) })) .connect_lazy_with(options) }) .acquire() .await .unwrap(); let conn = pool_conn.acquire().await.unwrap(); conn.execute(&format!("DROP DATABASE IF EXISTS {database}")[..]) .await .unwrap(); conn.execute(&format!("CREATE DATABASE {database}")[..]) .await .unwrap(); } let db_url = format!( "mysql://{}{}@{}:{}/{}", base_options.get_username(), if let Some(password) = maybe_password { format!(":{}", password) } else { "".to_string() }, base_options.get_host(), base_options.get_port(), database, ); let db_config: Map<_, Value> = map! { "url" => db_url.into(), }; let git_root = testdir!(); let git_hook = std::env::current_exe() .unwrap() .parent() .unwrap() .parent() .unwrap() .join("eyeballs-githook"); let authorized_keys = git_root.join("authorized_keys"); let figment = base_figment .merge(("databases", map!["eyeballs" => db_config])) .merge(("git_server_root", git_root)) .merge(("git_hook", git_hook)) .merge(("authorized_keys", authorized_keys)); Client::tracked(crate::rocket_from_config(figment)) .await .expect("valid rocket instance") } async fn get_status_from<'a>(request: LocalRequest<'a>) -> api_model::StatusResponse { request .header(&FAKE_IP) .dispatch() .await .into_json::() .await .unwrap() } async fn get_status<'a>(client: &Client) -> api_model::StatusResponse { get_status_from(client.get("/api/v1/status")).await } async fn login(client: &Client) { let login = get_status_from( client .post("/api/v1/login") .body("username=user&password=password") .header(ContentType::Form), ) .await; assert_eq!(login.ok, true); } async fn get_projects<'a>(client: &Client) -> api_model::Projects { client .get("/api/v1/projects") .header(&FAKE_IP) .dispatch() .await .into_json::() .await .unwrap() } async fn get_project_from<'a>(request: LocalRequest<'a>) -> api_model::Project { request .header(&FAKE_IP) .dispatch() .await .into_json::() .await .unwrap() } async fn get_users<'a>(client: &Client) -> api_model::Users { client .get("/api/v1/users") .header(&FAKE_IP) .dispatch() .await .into_json::() .await .unwrap() } async fn new_project(client: &Client) -> api_model::Project { get_project_from( client .post("/api/v1/project/test/new") .json(&api_model::ProjectData { title: Some("foo"), description: Some("bar"), remote: None, main_branch: Some("zod"), }), ) .await } async fn get_reviews<'a>(client: &Client, project_url: impl Display) -> api_model::Reviews { client .get(format!("{project_url}/reviews")) .header(&FAKE_IP) .dispatch() .await .into_json::() .await .unwrap() } async fn get_user_key_from<'a>(request: LocalRequest<'a>) -> api_model::UserKey { request .header(&FAKE_IP) .dispatch() .await .into_json::() .await .unwrap() } async fn get_user_keys<'a>(client: &Client) -> api_model::UserKeys { client .get("/api/v1/user/keys") .header(&FAKE_IP) .dispatch() .await .into_json::() .await .unwrap() } #[rocket::async_test] async fn test_not_logged_in_status() { let client = async_client_with_private_database(function_name!().to_string()).await; let not_logged_in = get_status(&client).await; assert_eq!(not_logged_in.ok, false); } #[rocket::async_test] async fn test_login_status() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let logged_in = get_status(&client).await; assert_eq!(logged_in.ok, true); } #[rocket::async_test] async fn test_bad_login_status() { let client = async_client_with_private_database(function_name!().to_string()).await; let bad_password = client .post("/api/v1/login") .body("username=user&password=incorrect") .header(ContentType::Form) .header(&FAKE_IP) .dispatch() .await; assert_eq!(bad_password.status(), Status::Unauthorized); let bad_username = client .post("/api/v1/login") .body("username=incorrect&password=password") .header(ContentType::Form) .header(&FAKE_IP) .dispatch() .await; assert_eq!(bad_username.status(), Status::Unauthorized); } #[rocket::async_test] async fn test_change_ip() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let new_ip = client .get("/api/v1/status") .header(&ANOTHER_FAKE_IP) .dispatch() .await; assert_eq!(new_ip.status(), Status::Unauthorized); } #[rocket::async_test] async fn test_logout() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let logged_in = get_status(&client).await; assert_eq!(logged_in.ok, true); let logout = get_status_from(client.get("/api/v1/logout")).await; assert_eq!(logout.ok, true); let not_logged_in = get_status(&client).await; assert_eq!(not_logged_in.ok, false); } #[rocket::async_test] async fn test_projects_empty() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let projects = get_projects(&client).await; assert_eq!(projects.total_count, 0); assert_eq!(projects.more, false); assert_eq!(projects.projects.len(), 0); } #[rocket::async_test] async fn test_project_new() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; assert_eq!(project.id, "test"); assert_eq!(project.title, "foo"); assert_eq!(project.description, "bar"); assert_eq!(project.remote, ""); assert_eq!(project.main_branch, "zod"); assert_eq!(project.users.len(), 1); let user = project.users.get(0).unwrap(); assert_eq!(user.user.id, "user"); assert_eq!(user.default_role, api_model::UserReviewRole::Reviewer); assert_eq!(user.maintainer, true); let projects = get_projects(&client).await; assert_eq!(projects.total_count, 1); assert_eq!(projects.more, false); assert_eq!(projects.projects.len(), 1); let project_entry = projects.projects.get(0).unwrap(); assert_eq!(project_entry.id, project.id); assert_eq!(project_entry.title, project.title); let project2 = get_project_from(client.get(format!("/api/v1/project/{}", project.id))).await; assert_eq!(project, project2); } #[rocket::async_test] async fn test_project_new_duplicate() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; new_project(&client).await; let duplicate_project = client .post("/api/v1/project/test/new") .json(&api_model::ProjectData { title: Some("foo"), description: Some("bar"), remote: None, main_branch: Some("zod"), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(duplicate_project.status(), Status::Conflict); let projects = get_projects(&client).await; assert_eq!(projects.total_count, 1); } #[rocket::async_test] async fn test_project_update() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = get_project_from(client.post("/api/v1/project/test/new").json( &api_model::ProjectData { title: Some("foo"), description: None, remote: None, main_branch: Some("fum"), }, )) .await; let project_url = format!("/api/v1/project/{}", project.id); let update = client .post(project_url.clone()) .json(&api_model::ProjectData { title: None, description: Some("bar"), remote: None, main_branch: Some("zod"), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update.status(), Status::Ok); let updated_project = get_project_from(client.get(project_url)).await; assert_eq!(updated_project.title, project.title); assert_eq!(updated_project.description, "bar"); assert_eq!(updated_project.remote, project.remote); assert_eq!(updated_project.main_branch, "zod"); } #[rocket::async_test] async fn test_project_update_unknown() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let update = client .post("/api/v1/project/does_not_exist") .json(&api_model::ProjectData { title: Some("foo"), description: Some("bar"), remote: None, main_branch: Some("zod"), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update.status(), Status::NotFound); } #[rocket::async_test] async fn test_project_new_user() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let users = get_users(&client).await; let other = users.users.iter().find(|u| u.id == "other").unwrap(); let new = client .post(format!("{project_url}/user/{}/new", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: Some(true), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(new.status(), Status::Ok); let updated_project = get_project_from(client.get(project_url)).await; assert_eq!(updated_project.users.len(), 2); let other_entry = updated_project .users .iter() .find(|ue| ue.user.id == other.id) .unwrap(); assert_eq!(other_entry.user, *other); assert_eq!(other_entry.default_role, api_model::UserReviewRole::Watcher); assert_eq!(other_entry.maintainer, true); } #[rocket::async_test] async fn test_project_new_user_duplicate() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let users = get_users(&client).await; let other = users.users.iter().find(|u| u.id == "other").unwrap(); let new = client .post(format!("{project_url}/user/{}/new", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: Some(true), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(new.status(), Status::Ok); let duplicate = client .post(format!("{project_url}/user/{}/new", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Reviewer), maintainer: Some(false), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(duplicate.status(), Status::Conflict); } #[rocket::async_test] async fn test_project_change_user() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let users = get_users(&client).await; let user = users.users.iter().find(|u| u.id == "user").unwrap(); let other = users.users.iter().find(|u| u.id == "other").unwrap(); let new = client .post(format!("{project_url}/user/{}/new", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: Some(true), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(new.status(), Status::Ok); let update = client .post(format!("{project_url}/user/{}", user.id)) .json(&api_model::ProjectUserEntryData { default_role: None, maintainer: Some(false), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update.status(), Status::Ok); let updated_project = get_project_from(client.get(project_url)).await; assert_eq!(updated_project.users.len(), 2); let user_entry = updated_project .users .iter() .find(|ue| ue.user.id == user.id) .unwrap(); assert_eq!(user_entry.user, *user); assert_eq!(user_entry.default_role, api_model::UserReviewRole::Reviewer); assert_eq!(user_entry.maintainer, false); } #[rocket::async_test] async fn test_project_change_user_unknown_project() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let users = get_users(&client).await; let other = users.users.iter().find(|u| u.id == "other").unwrap(); let update = client .post(format!("/api/v1/project/does_not_exist/user/{}", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: Some(true), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update.status(), Status::NotFound); } #[rocket::async_test] async fn test_project_change_user_unknown_user() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let update = client .post(format!("{project_url}/user/does_not_exist")) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: Some(true), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update.status(), Status::NotFound); } #[rocket::async_test] async fn test_project_change_user_not_in_project() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let users = get_users(&client).await; let other = users.users.iter().find(|u| u.id == "other").unwrap(); let update = client .post(format!("{project_url}/user/{}", other.id)) .json(&api_model::ProjectUserEntryData { default_role: None, maintainer: Some(false), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update.status(), Status::NotFound); } #[rocket::async_test] async fn test_project_check_maintainer() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let users = get_users(&client).await; let user = users.users.iter().find(|u| u.id == "user").unwrap(); let other = users.users.iter().find(|u| u.id == "other").unwrap(); let new = client .post(format!("{project_url}/user/{}/new", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: Some(true), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(new.status(), Status::Ok); let update = client .post(format!("{project_url}/user/{}", user.id)) .json(&api_model::ProjectUserEntryData { default_role: None, maintainer: Some(false), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update.status(), Status::Ok); let try_update_project = client .post(project_url.clone()) .json(&api_model::ProjectData { title: None, description: Some("fool"), remote: None, main_branch: None, }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(try_update_project.status(), Status::Unauthorized); let not_updated_project = get_project_from(client.get(project_url)).await; assert_eq!(not_updated_project.description, "bar"); } #[rocket::async_test] async fn test_project_dont_check_maintainer() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let users = get_users(&client).await; let user = users.users.iter().find(|u| u.id == "user").unwrap(); let other = users.users.iter().find(|u| u.id == "other").unwrap(); let new = client .post(format!("{project_url}/user/{}/new", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: Some(true), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(new.status(), Status::Ok); let update_maintainer = client .post(format!("{project_url}/user/{}", user.id)) .json(&api_model::ProjectUserEntryData { default_role: None, maintainer: Some(false), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update_maintainer.status(), Status::Ok); // Can still update default role for user ("me") let update_default_role = client .post(format!("{project_url}/user/{}", user.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: None, }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update_default_role.status(), Status::Ok); // But updating default role for other is no longer allowed. let update_other_default_role = client .post(format!("{project_url}/user/{}", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Reviewer), maintainer: None, }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(update_other_default_role.status(), Status::Unauthorized); } #[rocket::async_test] async fn test_project_delete_user() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let users = get_users(&client).await; let user = users.users.iter().find(|u| u.id == "user").unwrap(); let other = users.users.iter().find(|u| u.id == "other").unwrap(); let new = client .post(format!("{project_url}/user/{}/new", other.id)) .json(&api_model::ProjectUserEntryData { default_role: Some(api_model::UserReviewRole::Watcher), maintainer: Some(true), }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(new.status(), Status::Ok); let delete = client .delete(format!("{project_url}/user/{}", user.id)) .header(&FAKE_IP) .dispatch() .await; assert_eq!(delete.status(), Status::Ok); let updated_project = get_project_from(client.get(project_url)).await; assert_eq!(updated_project.users.len(), 1); let other_entry = updated_project.users.get(0).unwrap(); assert_eq!(other_entry.user, *other); } #[rocket::async_test] async fn test_project_delete_user_unknown_project() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let users = get_users(&client).await; let user = users.users.iter().find(|u| u.id == "user").unwrap(); let delete = client .delete(format!("/api/v1/project/does_not_exist/user/{}", user.id)) .header(&FAKE_IP) .dispatch() .await; assert_eq!(delete.status(), Status::NotFound); } #[rocket::async_test] async fn test_project_delete_user_unknown_user() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let delete = client .delete(format!("{project_url}/user/does_not_exist")) .header(&FAKE_IP) .dispatch() .await; assert_eq!(delete.status(), Status::NotFound); } #[rocket::async_test] async fn test_project_delete_user_not_in_project() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let users = get_users(&client).await; let other = users.users.iter().find(|u| u.id == "other").unwrap(); let delete = client .delete(format!("{project_url}/user/{}", other.id)) .header(&FAKE_IP) .dispatch() .await; assert_eq!(delete.status(), Status::NotFound); } #[rocket::async_test] async fn test_project_reviews_empty_project() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let project = new_project(&client).await; let project_url = format!("/api/v1/project/{}", project.id); let reviews = get_reviews(&client, project_url).await; assert_eq!(reviews.total_count, 0); assert_eq!(reviews.more, false); assert_eq!(reviews.reviews.len(), 0); } #[rocket::async_test] async fn test_project_reviews_unknown_project() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let reviews = client .get("/api/v1/project/does_not_exist/reviews") .header(&FAKE_IP) .dispatch() .await; assert_eq!(reviews.status(), Status::NotFound); } #[rocket::async_test] async fn test_user_keys_empty() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let user_keys = get_user_keys(&client).await; assert_eq!(user_keys.total_count, 0); assert_eq!(user_keys.more, false); assert_eq!(user_keys.keys.len(), 0); } #[rocket::async_test] async fn test_user_keys_add() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let key1 = get_user_key_from(client.post("/api/v1/user/keys/add").json( &api_model::UserKeyData { kind: "kind", data: "data", comment: None, }, )) .await; assert_eq!(key1.kind, "kind"); assert_eq!(key1.data, "data"); assert_eq!(key1.comment, ""); let mut user_keys = get_user_keys(&client).await; assert_eq!(user_keys.total_count, 1); assert_eq!(user_keys.more, false); assert_eq!(user_keys.keys.len(), 1); let key2 = user_keys.keys.get(0).unwrap(); assert_eq!(*key2, key1); let key3 = get_user_key_from(client.get(format!("/api/v1/user/keys/{}", key1.id))).await; assert_eq!(key3, key1); let key4 = get_user_key_from(client.post("/api/v1/user/keys/add").json( &api_model::UserKeyData { kind: "another-kind", data: "more.data", comment: Some("comment"), }, )) .await; user_keys = get_user_keys(&client).await; assert_eq!(user_keys.total_count, 2); assert_eq!(user_keys.more, false); assert_eq!(user_keys.keys.len(), 2); let key5 = user_keys.keys.get(0).unwrap(); let key6 = user_keys.keys.get(1).unwrap(); assert_eq!(*key5, key1); assert_eq!(*key6, key4); } #[rocket::async_test] async fn test_user_keys_add_invalid() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let result = client .post("/api/v1/user/keys/add") .json(&api_model::UserKeyData { kind: "kind with space", data: "data with space", comment: None, }) .header(&FAKE_IP) .dispatch() .await; assert_eq!(result.status(), Status::BadRequest); } #[rocket::async_test] async fn test_user_keys_del() { let client = async_client_with_private_database(function_name!().to_string()).await; login(&client).await; let key = get_user_key_from(client.post("/api/v1/user/keys/add").json( &api_model::UserKeyData { kind: "kind", data: "data", comment: None, }, )) .await; let delete = client .delete(format!("/api/v1/user/keys/{}", key.id)) .header(&FAKE_IP) .dispatch() .await; assert_eq!(delete.status(), Status::Ok); let user_keys = get_user_keys(&client).await; assert_eq!(user_keys.total_count, 0); assert_eq!(user_keys.more, false); assert_eq!(user_keys.keys.len(), 0); }