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::sync::OnceLock; use stdext::function_name; 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 figment = base_figment.merge(("databases", map!["eyeballs" => db_config])); 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/new") .json(&api_model::ProjectData { title: Some("foo"), description: Some("bar"), }), ) .await } #[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.title, "foo"); assert_eq!(project.description, "bar"); assert_eq!(project.users.len(), 1); let user = project.users.get(0).unwrap(); assert_eq!(user.user.username, "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_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/new").json( &api_model::ProjectData { title: Some("foo"), description: None, }, )) .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"), }) .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"); } #[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.username == "other").unwrap(); let new = client .post(format!("{project_url}/user/new?userid={}", 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_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.username == "user").unwrap(); let other = users.users.iter().find(|u| u.username == "other").unwrap(); let new = client .post(format!("{project_url}/user/new?userid={}", 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_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.username == "user").unwrap(); let other = users.users.iter().find(|u| u.username == "other").unwrap(); let new = client .post(format!("{project_url}/user/new?userid={}", 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"), }) .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.username == "user").unwrap(); let other = users.users.iter().find(|u| u.username == "other").unwrap(); let new = client .post(format!("{project_url}/user/new?userid={}", 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.username == "user").unwrap(); let other = users.users.iter().find(|u| u.username == "other").unwrap(); let new = client .post(format!("{project_url}/user/new?userid={}", 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); }