diff --git a/src/backend/api/article.rs b/src/backend/api/article.rs index ee21c45..b203ed1 100644 --- a/src/backend/api/article.rs +++ b/src/backend/api/article.rs @@ -1,3 +1,4 @@ +use crate::backend::api::ResolveObject; use crate::backend::database::article::DbArticleForm; use crate::backend::database::conflict::{ApiConflict, DbConflict, DbConflictForm}; use crate::backend::database::edit::DbEditForm; @@ -13,6 +14,7 @@ use crate::common::LocalUserView; use crate::common::{ArticleView, DbArticle, DbEdit}; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; +use anyhow::anyhow; use axum::extract::Query; use axum::Extension; use axum::Form; @@ -126,10 +128,20 @@ pub(in crate::backend::api) async fn get_article( Query(query): Query, data: Data, ) -> MyResult> { - Ok(Json(DbArticle::read_view_title( - &query.title, - &data.db_connection, - )?)) + match (query.title, query.id) { + (Some(title), None) => Ok(Json(DbArticle::read_view_title( + &title, + &query.instance_id, + &data.db_connection, + )?)), + (None, Some(id)) => { + if query.instance_id.is_some() { + return Err(anyhow!("Cant combine id and instance_id").into()); + } + Ok(Json(DbArticle::read_view(id, &data.db_connection)?)) + } + _ => Err(anyhow!("Must pass exactly one of title, id").into()), + } } #[derive(Deserialize, Serialize)] @@ -187,3 +199,20 @@ pub(in crate::backend::api) async fn fork_article( Ok(Json(DbArticle::read_view(article.id, &data.db_connection)?)) } + +/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new +/// article changes can only be received if we follow the instance, or if it is refetched manually. +#[debug_handler] +pub(super) async fn resolve_article( + Query(query): Query, + data: Data, +) -> MyResult> { + let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?; + let edits = DbEdit::read_for_article(&article, &data.db_connection)?; + let latest_version = edits.last().unwrap().hash.clone(); + Ok(Json(ArticleView { + article, + edits, + latest_version, + })) +} diff --git a/src/backend/api/instance.rs b/src/backend/api/instance.rs index ee9718f..7e7f688 100644 --- a/src/backend/api/instance.rs +++ b/src/backend/api/instance.rs @@ -1,9 +1,12 @@ +use crate::backend::api::ResolveObject; use crate::backend::database::instance::{DbInstance, InstanceView}; use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; use crate::backend::federation::activities::follow::Follow; use crate::common::LocalUserView; use activitypub_federation::config::Data; +use activitypub_federation::fetch::object_id::ObjectId; +use axum::extract::Query; use axum::Extension; use axum::{Form, Json}; use axum_macros::debug_handler; @@ -38,3 +41,16 @@ pub(in crate::backend::api) async fn follow_instance( Follow::send(user.person, instance, &data).await?; Ok(()) } + +/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to +/// the local instance, and allows for interactions such as following. +#[debug_handler] +pub(super) async fn resolve_instance( + Query(query): Query, + data: Data, +) -> MyResult> { + // TODO: workaround because axum makes it hard to have multiple routes on / + let id = format!("{}instance", query.id); + let instance: DbInstance = ObjectId::parse(&id)?.dereference(&data).await?; + Ok(Json(instance)) +} diff --git a/src/backend/api/mod.rs b/src/backend/api/mod.rs index 60b768b..0dd5ced 100644 --- a/src/backend/api/mod.rs +++ b/src/backend/api/mod.rs @@ -1,25 +1,20 @@ -use crate::backend::api::article::create_article; +use crate::backend::api::article::{create_article, resolve_article}; use crate::backend::api::article::{edit_article, fork_article, get_article}; -use crate::backend::api::instance::follow_instance; use crate::backend::api::instance::get_local_instance; -use crate::backend::api::user::my_profile; +use crate::backend::api::instance::{follow_instance, resolve_instance}; use crate::backend::api::user::register_user; use crate::backend::api::user::validate; use crate::backend::api::user::{login_user, logout_user}; +use crate::backend::api::user::{my_profile, AUTH_COOKIE}; use crate::backend::database::conflict::{ApiConflict, DbConflict}; -use crate::backend::database::instance::DbInstance; use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; -use crate::common::DbEdit; +use crate::common::DbArticle; use crate::common::LocalUserView; -use crate::common::{ArticleView, DbArticle}; use activitypub_federation::config::Data; -use activitypub_federation::fetch::object_id::ObjectId; use axum::extract::Query; use axum::routing::{get, post}; use axum::{ - extract::TypedHeader, - headers::authorization::{Authorization, Bearer}, http::Request, http::StatusCode, middleware::{self, Next}, @@ -27,6 +22,7 @@ use axum::{ Extension, }; use axum::{Json, Router}; +use axum_extra::extract::CookieJar; use axum_macros::debug_handler; use futures::future::try_join_all; use log::warn; @@ -44,11 +40,11 @@ pub fn api_routes() -> Router { get(get_article).post(create_article).patch(edit_article), ) .route("/article/fork", post(fork_article)) + .route("/article/resolve", get(resolve_article)) .route("/edit_conflicts", get(edit_conflicts)) - .route("/resolve_instance", get(resolve_instance)) - .route("/resolve_article", get(resolve_article)) .route("/instance", get(get_local_instance)) .route("/instance/follow", post(follow_instance)) + .route("/instance/resolve", get(resolve_instance)) .route("/search", get(search_article)) .route("/account/register", post(register_user)) .route("/account/login", post(login_user)) @@ -59,12 +55,12 @@ pub fn api_routes() -> Router { async fn auth( data: Data, - auth: Option>>, + jar: CookieJar, mut request: Request, next: Next, ) -> Result { - if let Some(auth) = auth { - let user = validate(auth.token(), &data).await.map_err(|e| { + if let Some(auth) = jar.get(AUTH_COOKIE) { + let user = validate(auth.value(), &data).await.map_err(|e| { warn!("Failed to validate auth token: {e}"); StatusCode::UNAUTHORIZED })?; @@ -79,36 +75,6 @@ pub struct ResolveObject { pub id: Url, } -/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to -/// the local instance, and allows for interactions such as following. -#[debug_handler] -async fn resolve_instance( - Query(query): Query, - data: Data, -) -> MyResult> { - // TODO: workaround because axum makes it hard to have multiple routes on / - let id = format!("{}instance", query.id); - let instance: DbInstance = ObjectId::parse(&id)?.dereference(&data).await?; - Ok(Json(instance)) -} - -/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new -/// article changes can only be received if we follow the instance, or if it is refetched manually. -#[debug_handler] -async fn resolve_article( - Query(query): Query, - data: Data, -) -> MyResult> { - let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?; - let edits = DbEdit::read_for_article(&article, &data.db_connection)?; - let latest_version = edits.last().unwrap().hash.clone(); - Ok(Json(ArticleView { - article, - edits, - latest_version, - })) -} - /// Get a list of all unresolved edit conflicts. #[debug_handler] async fn edit_conflicts( diff --git a/src/backend/api/user.rs b/src/backend/api/user.rs index 6fc5a4f..a2a4c65 100644 --- a/src/backend/api/user.rs +++ b/src/backend/api/user.rs @@ -60,7 +60,7 @@ pub(in crate::backend::api) async fn register_user( ) -> MyResult<(CookieJar, Json)> { let user = DbPerson::create_local(form.username, form.password, &data)?; let token = generate_login_token(&user.local_user, &data)?; - let jar = jar.add(create_cookie(token)); + let jar = jar.add(create_cookie(token, &data)); Ok((jar, Json(user))) } @@ -76,13 +76,18 @@ pub(in crate::backend::api) async fn login_user( return Err(anyhow!("Invalid login").into()); } let token = generate_login_token(&user.local_user, &data)?; - let jar = jar.add(create_cookie(token)); + let jar = jar.add(create_cookie(token, &data)); Ok((jar, Json(user))) } -fn create_cookie(jwt: String) -> Cookie<'static> { +fn create_cookie(jwt: String, data: &Data) -> Cookie<'static> { + let mut domain = data.domain().to_string(); + // remove port from domain + if domain.contains(':') { + domain = domain.split(':').collect::>()[0].to_string(); + } Cookie::build(AUTH_COOKIE, jwt) - .domain("localhost") + .domain(domain) .same_site(SameSite::Strict) .path("/") .http_only(true) diff --git a/src/backend/database/article.rs b/src/backend/database/article.rs index 845721d..a173395 100644 --- a/src/backend/database/article.rs +++ b/src/backend/database/article.rs @@ -66,7 +66,7 @@ impl DbArticle { article::table.find(id).get_result(conn.deref_mut())? }; let latest_version = article.latest_edit_version(conn)?; - let edits: Vec = DbEdit::read_for_article(&article, conn)?; + let edits = DbEdit::read_for_article(&article, conn)?; Ok(ArticleView { article, edits, @@ -74,15 +74,25 @@ impl DbArticle { }) } - pub fn read_view_title(title: &str, conn: &Mutex) -> MyResult { + pub fn read_view_title( + title: &str, + instance_id: &Option, + conn: &Mutex, + ) -> MyResult { let article: DbArticle = { let mut conn = conn.lock().unwrap(); - article::table - .filter(article::dsl::title.eq(title)) - .get_result(conn.deref_mut())? + let query = article::table + .into_boxed() + .filter(article::dsl::title.eq(title)); + let query = if let Some(instance_id) = instance_id { + query.filter(article::dsl::instance_id.eq(instance_id)) + } else { + query.filter(article::dsl::local.eq(true)) + }; + query.get_result(conn.deref_mut())? }; let latest_version = article.latest_edit_version(conn)?; - let edits: Vec = DbEdit::read_for_article(&article, conn)?; + let edits = DbEdit::read_for_article(&article, conn)?; Ok(ArticleView { article, edits, diff --git a/src/backend/mod.rs b/src/backend/mod.rs index d0949bc..0912133 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -71,9 +71,9 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> { // Create the main page which is shown by default let form = DbArticleForm { - title: "Main Page".to_string(), + title: "Main_Page".to_string(), text: "Hello world!".to_string(), - ap_id: ObjectId::parse("http://{hostname}/article/Main_Page")?, + ap_id: ObjectId::parse(&format!("http://{hostname}/article/Main_Page"))?, instance_id: instance.id, local: true, }; diff --git a/src/common/mod.rs b/src/common/mod.rs index cc7d583..2cc101c 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -8,9 +8,12 @@ use { diesel::{Identifiable, Queryable, Selectable}, }; +/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66 #[derive(Deserialize, Serialize, Clone)] pub struct GetArticleData { - pub title: String, + pub title: Option, + pub instance_id: Option, + pub id: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/src/frontend/api.rs b/src/frontend/api.rs index 850aabd..a0f5f9d 100644 --- a/src/frontend/api.rs +++ b/src/frontend/api.rs @@ -9,9 +9,52 @@ use serde::{Deserialize, Serialize}; pub static CLIENT: Lazy = Lazy::new(Client::new); -pub async fn get_article(hostname: &str, title: String) -> MyResult { - let get_article = GetArticleData { title }; - get_query::(hostname, "article", Some(get_article.clone())).await +#[derive(Clone)] +pub struct ApiClient { + // TODO: make these private + pub client: Client, + pub hostname: String, +} + +impl ApiClient { + pub fn new(client: Client, hostname: String) -> Self { + Self { client, hostname } + } + + async fn get_query(&self, endpoint: &str, query: Option) -> MyResult + where + T: for<'de> Deserialize<'de>, + R: Serialize, + { + let mut req = self + .client + .get(format!("http://{}/api/v1/{}", &self.hostname, endpoint)); + if let Some(query) = query { + req = req.query(&query); + } + handle_json_res::(req).await + } + + pub async fn get_article(&self, data: GetArticleData) -> MyResult { + self.get_query::("article", Some(data)) + .await + } + + pub async fn register(&self, register_form: RegisterUserData) -> MyResult { + let req = self + .client + .post(format!("http://{}/api/v1/account/register", self.hostname)) + .form(®ister_form); + handle_json_res::(req).await + } + + pub async fn login(&self, login_form: LoginUserData) -> MyResult { + let req = self + .client + .post(format!("http://{}/api/v1/account/login", self.hostname)) + .form(&login_form); + handle_json_res::(req).await + } } pub async fn get_query(hostname: &str, endpoint: &str, query: Option) -> MyResult @@ -40,20 +83,6 @@ where } } -pub async fn register(hostname: &str, register_form: RegisterUserData) -> MyResult { - let req = CLIENT - .post(format!("http://{}/api/v1/account/register", hostname)) - .form(®ister_form); - handle_json_res::(req).await -} - -pub async fn login(hostname: &str, login_form: LoginUserData) -> MyResult { - let req = CLIENT - .post(format!("http://{}/api/v1/account/login", hostname)) - .form(&login_form); - handle_json_res::(req).await -} - pub async fn my_profile(hostname: &str) -> MyResult { let req = CLIENT.get(format!("http://{}/api/v1/account/my_profile", hostname)); handle_json_res::(req).await diff --git a/src/frontend/app.rs b/src/frontend/app.rs index d209190..c921171 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -1,5 +1,5 @@ use crate::common::LocalUserView; -use crate::frontend::api::my_profile; +use crate::frontend::api::{my_profile, ApiClient}; use crate::frontend::components::nav::Nav; use crate::frontend::pages::article::Article; use crate::frontend::pages::login::Login; @@ -14,11 +14,14 @@ use leptos_meta::*; use leptos_router::Route; use leptos_router::Router; use leptos_router::Routes; +use reqwest::Client; // https://book.leptos.dev/15_global_state.html #[derive(Clone)] pub struct GlobalState { + // TODO: remove backend_hostname: String, + api_client: ApiClient, pub(crate) my_profile: Option, } @@ -29,6 +32,12 @@ impl GlobalState { .get_untracked() .backend_hostname } + pub fn api_client() -> ApiClient { + use_context::>() + .expect("global state is provided") + .get_untracked() + .api_client + } pub fn update_my_profile(&self) { let backend_hostname_ = self.backend_hostname.clone(); @@ -50,7 +59,8 @@ pub fn App() -> impl IntoView { provide_meta_context(); let backend_hostname = GlobalState { - backend_hostname, + backend_hostname: backend_hostname.clone(), + api_client: ApiClient::new(Client::new(), backend_hostname.clone()), my_profile: None, }; // Load user profile in case we are already logged in diff --git a/src/frontend/components/nav.rs b/src/frontend/components/nav.rs index 5925207..c088de5 100644 --- a/src/frontend/components/nav.rs +++ b/src/frontend/components/nav.rs @@ -7,7 +7,6 @@ use leptos_router::*; #[component] pub fn Nav() -> impl IntoView { let global_state = use_context::>().unwrap(); - // TODO: use `
  • diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index a3aab5b..ff8fef5 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -1,7 +1,7 @@ pub mod api; pub mod app; mod components; -mod error; +pub mod error; mod pages; #[cfg(feature = "hydrate")] diff --git a/src/frontend/pages/article.rs b/src/frontend/pages/article.rs index e9c6b83..b95d364 100644 --- a/src/frontend/pages/article.rs +++ b/src/frontend/pages/article.rs @@ -1,4 +1,5 @@ -use crate::frontend::api::get_article; +use crate::common::GetArticleData; +use crate::frontend::app::GlobalState; use leptos::*; use leptos_router::*; @@ -13,7 +14,16 @@ pub fn Article() -> impl IntoView { .cloned() .unwrap_or("Main Page".to_string()) }, - move |title| async move { get_article("localhost:8131", title).await.unwrap() }, + move |title| async move { + GlobalState::api_client() + .get_article(GetArticleData { + title: Some(title), + instance_id: None, + id: None, + }) + .await + .unwrap() + }, ); view! { diff --git a/src/frontend/pages/login.rs b/src/frontend/pages/login.rs index e8df78c..c508f97 100644 --- a/src/frontend/pages/login.rs +++ b/src/frontend/pages/login.rs @@ -1,5 +1,4 @@ use crate::common::LoginUserData; -use crate::frontend::api::login; use crate::frontend::app::GlobalState; use crate::frontend::components::credentials::*; use leptos::*; @@ -17,7 +16,7 @@ pub fn Login() -> impl IntoView { let credentials = LoginUserData { username, password }; async move { set_wait_for_response.update(|w| *w = true); - let result = login(&GlobalState::read_hostname(), credentials).await; + let result = GlobalState::api_client().login(credentials).await; set_wait_for_response.update(|w| *w = false); match result { Ok(res) => { diff --git a/src/frontend/pages/register.rs b/src/frontend/pages/register.rs index 847b41f..ef025df 100644 --- a/src/frontend/pages/register.rs +++ b/src/frontend/pages/register.rs @@ -1,7 +1,7 @@ -use crate::common::RegisterUserData; -use crate::frontend::api::register; +use crate::common::{LocalUserView, RegisterUserData}; use crate::frontend::app::GlobalState; use crate::frontend::components::credentials::*; +use crate::frontend::error::MyResult; use leptos::{logging::log, *}; #[component] @@ -17,7 +17,8 @@ pub fn Register() -> impl IntoView { log!("Try to register new account for {}", credentials.username); async move { set_wait_for_response.update(|w| *w = true); - let result = register(&GlobalState::read_hostname(), credentials).await; + let result: MyResult = + GlobalState::api_client().register(credentials).await; set_wait_for_response.update(|w| *w = false); match result { Ok(res) => { diff --git a/tests/common.rs b/tests/common.rs index 734fb3a..183ae61 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,23 +1,25 @@ use anyhow::anyhow; -use ibis::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData}; -use ibis::backend::api::instance::FollowInstance; -use ibis::backend::api::user::AUTH_COOKIE; -use ibis::backend::api::ResolveObject; -use ibis::backend::database::conflict::ApiConflict; -use ibis::backend::database::instance::DbInstance; -use ibis::backend::error::MyResult; -use ibis::backend::start; -use ibis::common::ArticleView; -use ibis::common::LoginUserData; -use ibis::common::RegisterUserData; -use ibis::frontend::api::{get_article, get_query, handle_json_res, register}; -use once_cell::sync::Lazy; -use reqwest::{Client, ClientBuilder, StatusCode}; +use ibis_lib::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData}; +use ibis_lib::backend::api::instance::FollowInstance; +use ibis_lib::backend::api::ResolveObject; +use ibis_lib::backend::database::conflict::ApiConflict; +use ibis_lib::backend::database::instance::DbInstance; +use ibis_lib::backend::start; +use ibis_lib::common::RegisterUserData; +use ibis_lib::common::{ArticleView, GetArticleData}; +use ibis_lib::frontend::api::ApiClient; +use ibis_lib::frontend::api::{get_query, handle_json_res}; +use ibis_lib::frontend::error::MyResult; + +use reqwest::cookie::Jar; +use reqwest::{ClientBuilder, StatusCode}; use serde::de::Deserialize; use std::env::current_dir; use std::fs::create_dir_all; +use std::ops::Deref; use std::process::{Command, Stdio}; use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::Arc; use std::sync::Once; use std::thread::{sleep, spawn}; use std::time::Duration; @@ -25,8 +27,6 @@ use tokio::task::JoinHandle; use tracing::log::LevelFilter; use url::Url; -pub static CLIENT: Lazy = Lazy::new(Client::new); - pub struct TestData { pub alpha: IbisInstance, pub beta: IbisInstance, @@ -95,9 +95,7 @@ fn generate_db_path(name: &'static str, port: i32) -> String { } pub struct IbisInstance { - pub hostname: String, - pub client: Client, - pub jwt: String, + pub api_client: ApiClient, db_path: String, db_handle: JoinHandle<()>, } @@ -127,14 +125,20 @@ impl IbisInstance { username: username.to_string(), password: "hunter2".to_string(), }; - // TODO: use a separate http client for each backend instance, with cookie store for auth - // TODO: how to pass the client/hostname to api client methods? - // probably create a struct ApiClient(hostname, client) with all api methods in impl - let client = ClientBuilder::new().cookie_store(true).build(); - let register_res = register(&hostname, form).await.unwrap(); + // use a separate http client for each backend instance, with cookie store for auth + // how to pass the client/hostname to api client methods? + // probably create a struct ApiClient(hostname, client) with all api methods in impl + // TODO: seems that cookie isnt being stored? or maybe wrong hostname? + let jar = Arc::new(Jar::default()); + let client = ClientBuilder::new() + .cookie_store(true) + .cookie_provider(jar.clone()) + .build() + .unwrap(); + let api_client = ApiClient::new(client, hostname.clone()); + api_client.register(form).await.unwrap(); Self { - hostname, - client, + api_client, db_path, db_handle: handle, } @@ -153,16 +157,27 @@ impl IbisInstance { } } +impl Deref for IbisInstance { + type Target = ApiClient; + + fn deref(&self) -> &Self::Target { + &self.api_client + } +} pub const TEST_ARTICLE_DEFAULT_TEXT: &str = "some\nexample\ntext\n"; pub async fn create_article(instance: &IbisInstance, title: String) -> MyResult { let create_form = CreateArticleData { title: title.clone(), }; - let req = CLIENT - .post(format!("http://{}/api/v1/article", &instance.hostname)) - .form(&create_form) - .bearer_auth(&instance.jwt); + let req = instance + .api_client + .client + .post(format!( + "http://{}/api/v1/article", + &instance.api_client.hostname + )) + .form(&create_form); let article: ArticleView = handle_json_res(req).await?; // create initial edit to ensure that conflicts are generated (there are no conflicts on empty file) @@ -172,28 +187,30 @@ pub async fn create_article(instance: &IbisInstance, title: String) -> MyResult< previous_version_id: article.latest_version, resolve_conflict_id: None, }; - edit_article(instance, &edit_form).await + Ok(edit_article(instance, &edit_form).await.unwrap()) } pub async fn edit_article_with_conflict( instance: &IbisInstance, edit_form: &EditArticleData, ) -> MyResult> { - let req = CLIENT - .patch(format!("http://{}/api/v1/article", instance.hostname)) - .form(edit_form) - .bearer_auth(&instance.jwt); - handle_json_res(req).await? + let req = instance + .api_client + .client + .patch(format!( + "http://{}/api/v1/article", + instance.api_client.hostname + )) + .form(edit_form); + handle_json_res(req).await } pub async fn get_conflicts(instance: &IbisInstance) -> MyResult> { - let req = CLIENT - .get(format!( - "http://{}/api/v1/edit_conflicts", - &instance.hostname - )) - .bearer_auth(&instance.jwt); - handle_json_res(req).await? + let req = instance.api_client.client.get(format!( + "http://{}/api/v1/edit_conflicts", + &instance.api_client.hostname + )); + Ok(handle_json_res(req).await.unwrap()) } pub async fn edit_article( @@ -203,51 +220,70 @@ pub async fn edit_article( let edit_res = edit_article_with_conflict(instance, edit_form).await?; assert!(edit_res.is_none()); - get_article(&instance.hostname, todo!("{}", edit_form.article_id)).await + instance + .api_client + .get_article(GetArticleData { + title: None, + instance_id: None, + id: Some(edit_form.article_id), + }) + .await } pub async fn get(hostname: &str, endpoint: &str) -> MyResult where T: for<'de> Deserialize<'de>, { - get_query(hostname, endpoint, None::).await + Ok(get_query(hostname, endpoint, None::).await.unwrap()) } pub async fn fork_article( instance: &IbisInstance, form: &ForkArticleData, ) -> MyResult { - let req = CLIENT - .post(format!("http://{}/api/v1/article/fork", instance.hostname)) - .form(form) - .bearer_auth(&instance.jwt); - handle_json_res(req).await? + let req = instance + .api_client + .client + .post(format!( + "http://{}/api/v1/article/fork", + instance.api_client.hostname + )) + .form(form); + Ok(handle_json_res(req).await.unwrap()) } -pub async fn follow_instance(instance: &IbisInstance, follow_instance: &str) -> MyResult<()> { +pub async fn follow_instance( + instance: &IbisInstance, + follow_instance: &str, +) -> MyResult { // fetch beta instance on alpha let resolve_form = ResolveObject { id: Url::parse(&format!("http://{}", follow_instance))?, }; - let instance_resolved: DbInstance = - get_query(&instance.hostname, "resolve_instance", Some(resolve_form)).await?; + let instance_resolved: DbInstance = get_query( + &instance.api_client.hostname, + "instance/resolve", + Some(resolve_form), + ) + .await?; // send follow let follow_form = FollowInstance { id: instance_resolved.id, }; // cant use post helper because follow doesnt return json - let res = CLIENT + let res = instance + .api_client + .client .post(format!( "http://{}/api/v1/instance/follow", - instance.hostname + instance.api_client.hostname )) .form(&follow_form) - .bearer_auth(&instance.jwt) .send() .await?; if res.status() == StatusCode::OK { - Ok(()) + Ok(instance_resolved) } else { Err(anyhow!("API error: {}", res.text().await?).into()) } diff --git a/tests/test.rs b/tests/test.rs index 3f2d86b..7280045 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,4 +1,4 @@ -extern crate ibis; +extern crate ibis_lib; mod common; @@ -6,23 +6,21 @@ use crate::common::fork_article; use crate::common::get_conflicts; use crate::common::{ create_article, edit_article, edit_article_with_conflict, follow_instance, get, TestData, - CLIENT, TEST_ARTICLE_DEFAULT_TEXT, + TEST_ARTICLE_DEFAULT_TEXT, }; -use ibis::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData}; -use ibis::backend::api::{ResolveObject, SearchArticleData}; -use ibis::backend::database::instance::{DbInstance, InstanceView}; -use ibis::backend::error::MyResult; -use ibis::common::ArticleView; -use ibis::common::DbArticle; -use ibis::frontend::api::get_article; -use ibis::frontend::api::get_query; -use ibis::frontend::api::handle_json_res; -use ibis::frontend::api::login; +use ibis_lib::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData}; +use ibis_lib::backend::api::{ResolveObject, SearchArticleData}; +use ibis_lib::backend::database::instance::{DbInstance, InstanceView}; +use ibis_lib::common::{ArticleView, GetArticleData}; +use ibis_lib::common::{DbArticle, LoginUserData, RegisterUserData}; +use ibis_lib::frontend::api::get_query; +use ibis_lib::frontend::api::handle_json_res; +use ibis_lib::frontend::error::MyResult; use pretty_assertions::{assert_eq, assert_ne}; use url::Url; #[tokio::test] -async fn test_create_read_and_edit_article() -> MyResult<()> { +async fn test_create_read_and_edit_local_article() -> MyResult<()> { let data = TestData::start().await; // create article @@ -32,13 +30,22 @@ async fn test_create_read_and_edit_article() -> MyResult<()> { assert!(create_res.article.local); // now article can be read - let get_res = get_article(&data.alpha.hostname, create_res.article.id).await?; + let get_article_data = GetArticleData { + title: Some(create_res.article.title.clone()), + instance_id: None, + id: None, + }; + let get_res = data + .alpha + .get_article(get_article_data.clone()) + .await + .unwrap(); assert_eq!(title, get_res.article.title); assert_eq!(TEST_ARTICLE_DEFAULT_TEXT, get_res.article.text); assert!(get_res.article.local); // error on article which wasnt federated - let not_found = get_article(&data.beta.hostname, create_res.article.id).await; + let not_found = data.beta.get_article(get_article_data.clone()).await; assert!(not_found.is_err()); // edit article @@ -56,7 +63,9 @@ async fn test_create_read_and_edit_article() -> MyResult<()> { query: title.clone(), }; let search_res: Vec = - get_query(&data.alpha.hostname, "search", Some(search_form)).await?; + get_query(&data.alpha.api_client.hostname, "search", Some(search_form)) + .await + .unwrap(); assert_eq!(1, search_res.len()); assert_eq!(edit_res.article, search_res[0]); @@ -134,23 +143,30 @@ async fn test_synchronize_articles() -> MyResult<()> { }; edit_article(&data.alpha, &edit_form).await?; - // article is not yet on beta - let get_res = get_article(&data.beta.hostname, create_res.article.id).await; - assert!(get_res.is_err()); - // fetch alpha instance on beta, articles are also fetched automatically let resolve_object = ResolveObject { id: Url::parse(&format!("http://{}", &data.alpha.hostname))?, }; - get_query::( + let instance: DbInstance = get_query::( &data.beta.hostname, - "resolve_instance", + "instance/resolve", Some(resolve_object), ) .await?; - // get the article and compare - let get_res = get_article(&data.beta.hostname, create_res.article.id).await?; + let mut get_article_data = GetArticleData { + title: Some(create_res.article.title), + instance_id: None, + id: None, + }; + + // try to read remote article by name, fails without domain + let get_res = data.beta.get_article(get_article_data.clone()).await; + assert!(get_res.is_err()); + + // get the article with instance id and compare + get_article_data.instance_id = Some(instance.id); + let get_res = data.beta.get_article(get_article_data).await?; assert_eq!(create_res.article.ap_id, get_res.article.ap_id); assert_eq!(title, get_res.article.title); assert_eq!(2, get_res.edits.len()); @@ -164,7 +180,7 @@ async fn test_synchronize_articles() -> MyResult<()> { async fn test_edit_local_article() -> MyResult<()> { let data = TestData::start().await; - follow_instance(&data.alpha, &data.beta.hostname).await?; + let beta_instance = follow_instance(&data.alpha, &data.beta.hostname).await?; // create new article let title = "Manu_Chao".to_string(); @@ -173,7 +189,12 @@ async fn test_edit_local_article() -> MyResult<()> { assert!(create_res.article.local); // article should be federated to alpha - let get_res = get_article(&data.alpha.hostname, create_res.article.id).await?; + let get_article_data = GetArticleData { + title: Some(create_res.article.title.to_string()), + instance_id: Some(beta_instance.id), + id: None, + }; + let get_res = data.alpha.get_article(get_article_data.clone()).await?; assert_eq!(create_res.article.title, get_res.article.title); assert_eq!(1, get_res.edits.len()); assert!(!get_res.article.local); @@ -195,7 +216,7 @@ async fn test_edit_local_article() -> MyResult<()> { .starts_with(&edit_res.article.ap_id.to_string())); // edit should be federated to alpha - let get_res = get_article(&data.alpha.hostname, edit_res.article.id).await?; + let get_res = data.alpha.get_article(get_article_data).await?; assert_eq!(edit_res.article.title, get_res.article.title); assert_eq!(edit_res.edits.len(), 2); assert_eq!(edit_res.article.text, get_res.article.text); @@ -207,27 +228,43 @@ async fn test_edit_local_article() -> MyResult<()> { async fn test_edit_remote_article() -> MyResult<()> { let data = TestData::start().await; - follow_instance(&data.alpha, &data.beta.hostname).await?; - follow_instance(&data.gamma, &data.beta.hostname).await?; + let beta_id_on_alpha = follow_instance(&data.alpha, &data.beta.hostname).await?; + let beta_id_on_gamma = follow_instance(&data.gamma, &data.beta.hostname).await?; // create new article let title = "Manu_Chao".to_string(); let create_res = create_article(&data.beta, title.clone()).await?; - assert_eq!(title, create_res.article.title); + assert_eq!(&title, &create_res.article.title); assert!(create_res.article.local); // article should be federated to alpha and gamma - let get_res = get_article(&data.alpha.hostname, create_res.article.id).await?; + let get_article_data_alpha = GetArticleData { + title: Some(create_res.article.title.to_string()), + instance_id: Some(beta_id_on_alpha.id), + id: None, + }; + let get_res = data + .alpha + .get_article(get_article_data_alpha.clone()) + .await?; assert_eq!(create_res.article.title, get_res.article.title); assert_eq!(1, get_res.edits.len()); assert!(!get_res.article.local); - let get_res = get_article(&data.gamma.hostname, create_res.article.id).await?; + let get_article_data_gamma = GetArticleData { + title: Some(create_res.article.title.to_string()), + instance_id: Some(beta_id_on_gamma.id), + id: None, + }; + let get_res = data + .gamma + .get_article(get_article_data_gamma.clone()) + .await?; assert_eq!(create_res.article.title, get_res.article.title); assert_eq!(create_res.article.text, get_res.article.text); let edit_form = EditArticleData { - article_id: create_res.article.id, + article_id: get_res.article.id, new_text: "Lorem Ipsum 2".to_string(), previous_version_id: get_res.latest_version, resolve_conflict_id: None, @@ -242,12 +279,12 @@ async fn test_edit_remote_article() -> MyResult<()> { .starts_with(&edit_res.article.ap_id.to_string())); // edit should be federated to beta and gamma - let get_res = get_article(&data.alpha.hostname, create_res.article.id).await?; + let get_res = data.alpha.get_article(get_article_data_alpha).await?; assert_eq!(edit_res.article.title, get_res.article.title); assert_eq!(edit_res.edits.len(), 2); assert_eq!(edit_res.article.text, get_res.article.text); - let get_res = get_article(&data.gamma.hostname, create_res.article.id).await?; + let get_res = data.gamma.get_article(get_article_data_gamma).await?; assert_eq!(edit_res.article.title, get_res.article.title); assert_eq!(edit_res.edits.len(), 2); assert_eq!(edit_res.article.text, get_res.article.text); @@ -311,7 +348,7 @@ async fn test_local_edit_conflict() -> MyResult<()> { async fn test_federated_edit_conflict() -> MyResult<()> { let data = TestData::start().await; - follow_instance(&data.alpha, &data.beta.hostname).await?; + let beta_id_on_alpha = follow_instance(&data.alpha, &data.beta.hostname).await?; // create new article let title = "Manu_Chao".to_string(); @@ -325,15 +362,23 @@ async fn test_federated_edit_conflict() -> MyResult<()> { }; let resolve_res: ArticleView = get_query( &data.gamma.hostname, - "resolve_article", + "article/resolve", Some(resolve_object), ) .await?; assert_eq!(create_res.article.text, resolve_res.article.text); // alpha edits article + let get_article_data = GetArticleData { + title: Some(title.to_string()), + instance_id: Some(beta_id_on_alpha.id), + id: None, + }; + let get_res = data.alpha.get_article(get_article_data).await?; + assert_eq!(&create_res.edits.len(), &get_res.edits.len()); + assert_eq!(&create_res.edits[0].hash, &get_res.edits[0].hash); let edit_form = EditArticleData { - article_id: create_res.article.id, + article_id: get_res.article.id, new_text: "Lorem Ipsum\n".to_string(), previous_version_id: create_res.latest_version.clone(), resolve_conflict_id: None, @@ -350,7 +395,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { // gamma also edits, as its not the latest version there is a conflict. local version should // not be updated with this conflicting version, instead user needs to handle the conflict let edit_form = EditArticleData { - article_id: create_res.article.id, + article_id: resolve_res.article.id, new_text: "aaaa\n".to_string(), previous_version_id: create_res.latest_version, resolve_conflict_id: None, @@ -365,7 +410,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { // resolve the conflict let edit_form = EditArticleData { - article_id: create_res.article.id, + article_id: resolve_res.article.id, new_text: "aaaa\n".to_string(), previous_version_id: conflicts[0].previous_version_id.clone(), resolve_conflict_id: Some(conflicts[0].id.clone()), @@ -432,7 +477,7 @@ async fn test_fork_article() -> MyResult<()> { id: create_res.article.ap_id.into_inner(), }; let resolve_res: ArticleView = - get_query(&data.beta.hostname, "resolve_article", Some(resolve_object)).await?; + get_query(&data.beta.hostname, "article/resolve", Some(resolve_object)).await?; let resolved_article = resolve_res.article; assert_eq!(create_res.edits.len(), resolve_res.edits.len()); @@ -471,24 +516,36 @@ async fn test_user_registration_login() -> MyResult<()> { let data = TestData::start().await; let username = "my_user"; let password = "hunter2"; - let register = register(&data.alpha.hostname, username, password).await?; - assert!(!register.jwt.is_empty()); + let register_data = RegisterUserData { + username: username.to_string(), + password: password.to_string(), + }; + data.alpha.register(register_data).await?; - let invalid_login = login(&data.alpha, username, "asd123").await; + let login_data = LoginUserData { + username: username.to_string(), + password: "asd123".to_string(), + }; + let invalid_login = data.alpha.login(login_data).await; assert!(invalid_login.is_err()); - let valid_login = login(&data.alpha, username, password).await?; - assert!(!valid_login.jwt.is_empty()); + let login_data = LoginUserData { + username: username.to_string(), + password: password.to_string(), + }; + data.alpha.login(login_data).await?; let title = "Manu_Chao".to_string(); let create_form = CreateArticleData { title: title.clone(), }; - let req = CLIENT + let req = data + .alpha + .api_client + .client .post(format!("http://{}/api/v1/article", &data.alpha.hostname)) - .form(&create_form) - .bearer_auth(valid_login.jwt); + .form(&create_form); let create_res: ArticleView = handle_json_res(req).await?; assert_eq!(title, create_res.article.title);