fix integration tests

This commit is contained in:
Felix Ableitner 2024-01-17 16:40:01 +01:00
parent 8ca01dee07
commit 150415e8ad
16 changed files with 365 additions and 195 deletions

View File

@ -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<GetArticleData>,
data: Data<MyDataHandle>,
) -> MyResult<Json<ArticleView>> {
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<ResolveObject>,
data: Data<MyDataHandle>,
) -> MyResult<Json<ArticleView>> {
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,
}))
}

View File

@ -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<ResolveObject>,
data: Data<MyDataHandle>,
) -> MyResult<Json<DbInstance>> {
// 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))
}

View File

@ -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<B>(
data: Data<MyDataHandle>,
auth: Option<TypedHeader<Authorization<Bearer>>>,
jar: CookieJar,
mut request: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode> {
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<ResolveObject>,
data: Data<MyDataHandle>,
) -> MyResult<Json<DbInstance>> {
// 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<ResolveObject>,
data: Data<MyDataHandle>,
) -> MyResult<Json<ArticleView>> {
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(

View File

@ -60,7 +60,7 @@ pub(in crate::backend::api) async fn register_user(
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
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<MyDataHandle>) -> Cookie<'static> {
let mut domain = data.domain().to_string();
// remove port from domain
if domain.contains(':') {
domain = domain.split(':').collect::<Vec<_>>()[0].to_string();
}
Cookie::build(AUTH_COOKIE, jwt)
.domain("localhost")
.domain(domain)
.same_site(SameSite::Strict)
.path("/")
.http_only(true)

View File

@ -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> = 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<PgConnection>) -> MyResult<ArticleView> {
pub fn read_view_title(
title: &str,
instance_id: &Option<i32>,
conn: &Mutex<PgConnection>,
) -> MyResult<ArticleView> {
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> = DbEdit::read_for_article(&article, conn)?;
let edits = DbEdit::read_for_article(&article, conn)?;
Ok(ArticleView {
article,
edits,

View File

@ -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,
};

View File

@ -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<String>,
pub instance_id: Option<i32>,
pub id: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]

View File

@ -9,9 +9,52 @@ use serde::{Deserialize, Serialize};
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
pub async fn get_article(hostname: &str, title: String) -> MyResult<ArticleView> {
let get_article = GetArticleData { title };
get_query::<ArticleView, _>(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<T, R>(&self, endpoint: &str, query: Option<R>) -> MyResult<T>
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::<T>(req).await
}
pub async fn get_article(&self, data: GetArticleData) -> MyResult<ArticleView> {
self.get_query::<ArticleView, _>("article", Some(data))
.await
}
pub async fn register(&self, register_form: RegisterUserData) -> MyResult<LocalUserView> {
let req = self
.client
.post(format!("http://{}/api/v1/account/register", self.hostname))
.form(&register_form);
handle_json_res::<LocalUserView>(req).await
}
pub async fn login(&self, login_form: LoginUserData) -> MyResult<LocalUserView> {
let req = self
.client
.post(format!("http://{}/api/v1/account/login", self.hostname))
.form(&login_form);
handle_json_res::<LocalUserView>(req).await
}
}
pub async fn get_query<T, R>(hostname: &str, endpoint: &str, query: Option<R>) -> MyResult<T>
@ -40,20 +83,6 @@ where
}
}
pub async fn register(hostname: &str, register_form: RegisterUserData) -> MyResult<LocalUserView> {
let req = CLIENT
.post(format!("http://{}/api/v1/account/register", hostname))
.form(&register_form);
handle_json_res::<LocalUserView>(req).await
}
pub async fn login(hostname: &str, login_form: LoginUserData) -> MyResult<LocalUserView> {
let req = CLIENT
.post(format!("http://{}/api/v1/account/login", hostname))
.form(&login_form);
handle_json_res::<LocalUserView>(req).await
}
pub async fn my_profile(hostname: &str) -> MyResult<LocalUserView> {
let req = CLIENT.get(format!("http://{}/api/v1/account/my_profile", hostname));
handle_json_res::<LocalUserView>(req).await

View File

@ -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<LocalUserView>,
}
@ -29,6 +32,12 @@ impl GlobalState {
.get_untracked()
.backend_hostname
}
pub fn api_client() -> ApiClient {
use_context::<RwSignal<GlobalState>>()
.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

View File

@ -7,7 +7,6 @@ use leptos_router::*;
#[component]
pub fn Nav() -> impl IntoView {
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
// TODO: use `<Show when` based on auth token for login/register/logout
view! {
<nav class="inner">
<li>

View File

@ -1,7 +1,7 @@
pub mod api;
pub mod app;
mod components;
mod error;
pub mod error;
mod pages;
#[cfg(feature = "hydrate")]

View File

@ -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! {

View File

@ -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) => {

View File

@ -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<LocalUserView> =
GlobalState::api_client().register(credentials).await;
set_wait_for_response.update(|w| *w = false);
match result {
Ok(res) => {

View File

@ -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<Client> = 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<ArticleView> {
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<Option<ApiConflict>> {
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<Vec<ApiConflict>> {
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<T>(hostname: &str, endpoint: &str) -> MyResult<T>
where
T: for<'de> Deserialize<'de>,
{
get_query(hostname, endpoint, None::<i32>).await
Ok(get_query(hostname, endpoint, None::<i32>).await.unwrap())
}
pub async fn fork_article(
instance: &IbisInstance,
form: &ForkArticleData,
) -> MyResult<ArticleView> {
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<DbInstance> {
// 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())
}

View File

@ -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<DbArticle> =
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::<DbInstance, _>(
let instance: DbInstance = get_query::<DbInstance, _>(
&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);