use pretty_assertions::assert_eq; 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::path::PathBuf; use std::sync::OnceLock; use stdext::function_name; use testdir::testdir; use crate::api_model; use crate::trans; 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, remote_key: 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.remote_key_abbrev, ""); 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, remote_key: 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, remote_key: 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, remote_key: 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.remote_key_abbrev, project.remote_key_abbrev); 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, remote_key: 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, remote_key: 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); } #[tokio::test] async fn test_collect_strings() { let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("common/src/testdata/grit"); let mut strings = trans::collect_strings(base, vec!["base.grd".to_string()]) .await .unwrap(); // Sort translations before comparison, they come unsorted from collect_strings. for string in &mut strings { string .translations .sort_by(|a, b| a.language.cmp(&b.language)); } assert_eq!( strings, vec![ api_model::LocalizationString { id: "IDS_EXTRA".to_string(), file: "extra.grdp".to_string(), description: "Extra title".to_string(), meaning: "".to_string(), source: "Extra title".to_string(), placeholders: vec![], placeholder_offset: vec![], translation_id: 3567801202192813433, translations: vec![], }, api_model::LocalizationString { id: "IDS_BOOKMARKS_FRAGMENT_TITLE".to_string(), file: "base.grd".to_string(), description: "Title which is shown on the main bookmarks view.".to_string(), meaning: "".to_string(), source: "Bookmarks".to_string(), placeholders: vec![], placeholder_offset: vec![], translation_id: 8820817407110198400, translations: vec![ api_model::TranslationString { language: "en-gb".to_string(), translation: "Bookmarks".to_string(), placeholder_offset: vec![], }, api_model::TranslationString { language: "my".to_string(), translation: "ဝက်ဘ်လိပ်စာ မှတ်ထားမှုများ".to_string(), placeholder_offset: vec![], }, api_model::TranslationString { language: "my-zg".to_string(), translation: "ဝက္ဘ္လိပ္စာ မွတ္ထားမွုမ်ား".to_string(), placeholder_offset: vec![], }, api_model::TranslationString { language: "sv".to_string(), translation: "Bokmärken".to_string(), placeholder_offset: vec![], }, ], }, api_model::LocalizationString { id: "IDS_GENERIC_WELCOME".to_string(), file: "base.grd".to_string(), description: "Generic welcome string.".to_string(), meaning: "".to_string(), source: "Welcome to ".to_string(), placeholders: vec![ api_model::LocalizationPlaceholder { id: "STRING".to_string(), content: "%1$s".to_string(), example: "Opera".to_string(), }, ], placeholder_offset: vec![11], translation_id: 8443102241046796905, translations: vec![ api_model::TranslationString { language: "en-gb".to_string(), translation: "Welcome to ".to_string(), placeholder_offset: vec![11], }, api_model::TranslationString { language: "my".to_string(), translation: " မှ ကြိုဆိုပါသည်".to_string(), placeholder_offset: vec![0], }, api_model::TranslationString { language: "my-zg".to_string(), translation: " မွ ႀကိဳဆိုပါသည္".to_string(), placeholder_offset: vec![0], }, api_model::TranslationString { language: "sv".to_string(), translation: "Välkommen till ".to_string(), placeholder_offset: vec![16], }, ], }, api_model::LocalizationString { id: "IDS_START_TERMS".to_string(), file: "base.grd".to_string(), description: "First startup information about the license and privacy terms.".to_string(), meaning: "".to_string(), source: "By using this application you are agreeing to Opera's Terms of Service. Also, you can learn how Opera handles and protects your data in our Privacy Statement.".to_string(), placeholders: vec![ api_model::LocalizationPlaceholder { id: "TOS_BEGIN".to_string(), content: "".to_string(), example: "".to_string(), }, api_model::LocalizationPlaceholder { id: "TOS_END".to_string(), content: "".to_string(), example: "".to_string(), }, api_model::LocalizationPlaceholder { id: "PRIVACY_BEGIN".to_string(), content: "".to_string(), example: "".to_string(), }, api_model::LocalizationPlaceholder { id: "PRIVACY_END".to_string(), content: "".to_string(), example: "".to_string(), }, ], placeholder_offset: vec![54, 70, 140, 157], translation_id: 2466140279568640908, translations: vec![ api_model::TranslationString { language: "en-gb".to_string(), translation: "By using this application you are agreeing to Opera's Terms of Service. Also, you can learn how Opera handles and protects your data in our Privacy Statement.".to_string(), placeholder_offset: vec![54, 70, 140, 157], }, api_model::TranslationString { language: "my".to_string(), translation: "ဤအပလီကေးရှင်းကို အသုံးပြုခြင်းဖြင့် သင်သည် Opera ၏ ဝန်ဆောင်မှုစည်းမျဉ်းများ ကို သဘောတူရာ ရောက်ပါသည်။ ထို့အပြင် ကျွန်ုပ်တို့၏ကိုယ်ရေးလုံခြုံမှု ထုတ်ပြန်ချက် ထဲတွင် သင့်ဒေတာများကို Opera ၏ ကိုင်တွယ်ပုံနှင့် ကာကွယ်ပုံတို့ကိုလည်း လေ့လာနိုင်သည်။".to_string(), placeholder_offset: vec![133, 205, 342, 433], }, api_model::TranslationString { language: "my-zg".to_string(), translation: "ဤအပလီေကးရွင္းကို အသုံးျပဳျခင္းျဖင့္ သင္သည္ Opera ၏ ဝန္ေဆာင္မွုစည္းမ်ဥ္းမ်ား ကို သေဘာတူရာ ေရာက္ပါသည္။ ထို႔အျပင္ ကၽြန္ုပ္တို႔၏ကိုယ္ေရးလုံျခဳံမွု ထုတ္ျပန္ခ်က္ ထဲတြင္ သင့္ေဒတာမ်ားကို Opera ၏ ကိုင္တြယ္ပုံႏွင့္ ကာကြယ္ပုံတို႔ကိုလည္း ေလ့လာနိုင္သည္။".to_string(), placeholder_offset: vec![133, 205, 342, 433], }, api_model::TranslationString { language: "sv".to_string(), translation: "I och med din användning av det här programmet samtycker du till Operas Licensvillkor. Du kan också läsa om hur Opera hanterar och skyddar dina data i vårt Sekretessmeddelande.".to_string(), placeholder_offset: vec![74, 87, 161, 180], }, ], }, api_model::LocalizationString { id: "IDS_BOOKMARKS_FOLDERS_DELETED".to_string(), file: "base.grd".to_string(), description: "Message which is shown when one or more folders have been deleted from the bookmark list.".to_string(), meaning: "".to_string(), source: "{BOOKMARKS, plural,\n one { folder deleted}\n few { folders deleted}\n many { folders deleted}\n other { folders deleted}}".to_string(), placeholders: vec![ api_model::LocalizationPlaceholder { id: "COUNT".to_string(), content: "%1$d".to_string(), example: "1".to_string(), }, api_model::LocalizationPlaceholder { id: "COUNT".to_string(), content: "%1$d".to_string(), example: "15".to_string(), }, api_model::LocalizationPlaceholder { id: "COUNT".to_string(), content: "%1$d".to_string(), example: "100".to_string(), }, api_model::LocalizationPlaceholder { id: "COUNT".to_string(), content: "%1$d".to_string(), example: "42".to_string(), }, ], placeholder_offset: vec![34, 65, 98, 132], translation_id: 7770247413830876286, translations: vec![ api_model::TranslationString { language: "en-gb".to_string(), translation: "{BOOKMARKS, plural,\n one { folder deleted}\n few { folders deleted}\n many { folders deleted}\n other { folders deleted}}".to_string(), placeholder_offset: vec![35, 67, 101, 136], }, ], }, ], ) }