move some api methods to struct

This commit is contained in:
Felix Ableitner 2024-01-17 17:14:16 +01:00
parent 150415e8ad
commit 173249ea8e
3 changed files with 158 additions and 151 deletions

View File

@ -1,11 +1,17 @@
use crate::common::GetArticleData;
use crate::backend::api::article::{CreateArticleData, EditArticleData};
use crate::backend::api::instance::FollowInstance;
use crate::backend::api::{ResolveObject, SearchArticleData};
use crate::backend::database::conflict::ApiConflict;
use crate::backend::database::instance::{DbInstance, InstanceView};
use crate::common::LocalUserView;
use crate::common::{ArticleView, LoginUserData, RegisterUserData};
use crate::common::{DbArticle, GetArticleData};
use crate::frontend::error::MyResult;
use anyhow::anyhow;
use once_cell::sync::Lazy;
use reqwest::{Client, RequestBuilder};
use reqwest::{Client, RequestBuilder, StatusCode};
use serde::{Deserialize, Serialize};
use url::Url;
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
@ -55,6 +61,84 @@ impl ApiClient {
.form(&login_form);
handle_json_res::<LocalUserView>(req).await
}
pub async fn create_article(&self, title: String, new_text: String) -> MyResult<ArticleView> {
let create_form = CreateArticleData {
title: title.clone(),
};
let req = self
.client
.post(format!("http://{}/api/v1/article", &self.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)
// TODO: maybe take initial text directly in create article, no reason to have empty article
let edit_form = EditArticleData {
article_id: article.article.id,
new_text,
previous_version_id: article.latest_version,
resolve_conflict_id: None,
};
Ok(self.edit_article(&edit_form).await.unwrap())
}
pub async fn edit_article_with_conflict(
&self,
edit_form: &EditArticleData,
) -> MyResult<Option<ApiConflict>> {
let req = self
.client
.patch(format!("http://{}/api/v1/article", self.hostname))
.form(edit_form);
handle_json_res(req).await
}
pub async fn edit_article(&self, edit_form: &EditArticleData) -> MyResult<ArticleView> {
let edit_res = self.edit_article_with_conflict(edit_form).await?;
assert!(edit_res.is_none());
self.get_article(GetArticleData {
title: None,
instance_id: None,
id: Some(edit_form.article_id),
})
.await
}
pub async fn search(&self, search_form: &SearchArticleData) -> MyResult<Vec<DbArticle>> {
self.get_query("search", Some(search_form)).await
}
pub async fn get_local_instance(&self) -> MyResult<InstanceView> {
self.get_query("instance", None::<i32>).await
}
pub async fn follow_instance(&self, 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(&self.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 = self
.client
.post(format!("http://{}/api/v1/instance/follow", self.hostname))
.form(&follow_form)
.send()
.await?;
if res.status() == StatusCode::OK {
Ok(instance_resolved)
} else {
Err(anyhow!("API error: {}", res.text().await?).into())
}
}
}
pub async fn get_query<T, R>(hostname: &str, endpoint: &str, query: Option<R>) -> MyResult<T>
@ -83,11 +167,13 @@ where
}
}
// TODO: cover in integration test
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
}
// TODO: cover in integration test
pub async fn logout(hostname: &str) -> MyResult<()> {
CLIENT
.get(format!("http://{}/api/v1/account/logout", hostname))

View File

@ -1,12 +1,12 @@
use anyhow::anyhow;
use ibis_lib::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
use ibis_lib::backend::api::article::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::ArticleView;
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;
@ -166,45 +166,6 @@ impl Deref for IbisInstance {
}
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 = 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)
let edit_form = EditArticleData {
article_id: article.article.id,
new_text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
previous_version_id: article.latest_version,
resolve_conflict_id: None,
};
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 = 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 = instance.api_client.client.get(format!(
"http://{}/api/v1/edit_conflicts",
@ -213,30 +174,6 @@ pub async fn get_conflicts(instance: &IbisInstance) -> MyResult<Vec<ApiConflict>
Ok(handle_json_res(req).await.unwrap())
}
pub async fn edit_article(
instance: &IbisInstance,
edit_form: &EditArticleData,
) -> MyResult<ArticleView> {
let edit_res = edit_article_with_conflict(instance, edit_form).await?;
assert!(edit_res.is_none());
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>,
{
Ok(get_query(hostname, endpoint, None::<i32>).await.unwrap())
}
pub async fn fork_article(
instance: &IbisInstance,
form: &ForkArticleData,
@ -251,40 +188,3 @@ pub async fn fork_article(
.form(form);
Ok(handle_json_res(req).await.unwrap())
}
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.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 = instance
.api_client
.client
.post(format!(
"http://{}/api/v1/instance/follow",
instance.api_client.hostname
))
.form(&follow_form)
.send()
.await?;
if res.status() == StatusCode::OK {
Ok(instance_resolved)
} else {
Err(anyhow!("API error: {}", res.text().await?).into())
}
}

View File

@ -4,10 +4,7 @@ mod common;
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,
TEST_ARTICLE_DEFAULT_TEXT,
};
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
use ibis_lib::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
use ibis_lib::backend::api::{ResolveObject, SearchArticleData};
use ibis_lib::backend::database::instance::{DbInstance, InstanceView};
@ -25,7 +22,10 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
// create article
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.alpha, title.clone()).await?;
let create_res = data
.alpha
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(title, create_res.article.title);
assert!(create_res.article.local);
@ -35,11 +35,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
instance_id: None,
id: None,
};
let get_res = data
.alpha
.get_article(get_article_data.clone())
.await
.unwrap();
let get_res = data.alpha.get_article(get_article_data.clone()).await?;
assert_eq!(title, get_res.article.title);
assert_eq!(TEST_ARTICLE_DEFAULT_TEXT, get_res.article.text);
assert!(get_res.article.local);
@ -55,17 +51,14 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = edit_article(&data.alpha, &edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(2, edit_res.edits.len());
let search_form = SearchArticleData {
query: title.clone(),
};
let search_res: Vec<DbArticle> =
get_query(&data.alpha.api_client.hostname, "search", Some(search_form))
.await
.unwrap();
let search_res = data.alpha.search(&search_form).await?;
assert_eq!(1, search_res.len());
assert_eq!(edit_res.article, search_res[0]);
@ -78,11 +71,17 @@ async fn test_create_duplicate_article() -> MyResult<()> {
// create article
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.alpha, title.clone()).await?;
let create_res = data
.alpha
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(title, create_res.article.title);
assert!(create_res.article.local);
let create_res = create_article(&data.alpha, title.clone()).await;
let create_res = data
.alpha
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await;
assert!(create_res.is_err());
data.stop()
@ -93,17 +92,17 @@ async fn test_follow_instance() -> MyResult<()> {
let data = TestData::start().await;
// check initial state
let alpha_instance: InstanceView = get(&data.alpha.hostname, "instance").await?;
let alpha_instance = data.alpha.get_local_instance().await?;
assert_eq!(0, alpha_instance.followers.len());
assert_eq!(0, alpha_instance.following.len());
let beta_instance: InstanceView = get(&data.beta.hostname, "instance").await?;
let beta_instance = data.beta.get_local_instance().await?;
assert_eq!(0, beta_instance.followers.len());
assert_eq!(0, beta_instance.following.len());
follow_instance(&data.alpha, &data.beta.hostname).await?;
data.alpha.follow_instance(&data.beta.hostname).await?;
// check that follow was federated
let alpha_instance: InstanceView = get(&data.alpha.hostname, "instance").await?;
let alpha_instance = data.alpha.get_local_instance().await?;
assert_eq!(1, alpha_instance.following.len());
assert_eq!(0, alpha_instance.followers.len());
assert_eq!(
@ -111,7 +110,7 @@ async fn test_follow_instance() -> MyResult<()> {
alpha_instance.following[0].ap_id
);
let beta_instance: InstanceView = get(&data.beta.hostname, "instance").await?;
let beta_instance = data.beta.get_local_instance().await?;
assert_eq!(0, beta_instance.following.len());
assert_eq!(1, beta_instance.followers.len());
// TODO: compare full ap_id of alpha user, but its not available through api yet
@ -129,7 +128,10 @@ async fn test_synchronize_articles() -> MyResult<()> {
// create article on alpha
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.alpha, title.clone()).await?;
let create_res = data
.alpha
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(title, create_res.article.title);
assert_eq!(1, create_res.edits.len());
assert!(create_res.article.local);
@ -141,7 +143,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
edit_article(&data.alpha, &edit_form).await?;
data.alpha.edit_article(&edit_form).await?;
// fetch alpha instance on beta, articles are also fetched automatically
let resolve_object = ResolveObject {
@ -180,11 +182,14 @@ async fn test_synchronize_articles() -> MyResult<()> {
async fn test_edit_local_article() -> MyResult<()> {
let data = TestData::start().await;
let beta_instance = follow_instance(&data.alpha, &data.beta.hostname).await?;
let beta_instance = data.alpha.follow_instance(&data.beta.hostname).await?;
// create new article
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.beta, title.clone()).await?;
let create_res = data
.beta
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(title, create_res.article.title);
assert!(create_res.article.local);
@ -207,7 +212,7 @@ async fn test_edit_local_article() -> MyResult<()> {
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = edit_article(&data.beta, &edit_form).await?;
let edit_res = data.beta.edit_article(&edit_form).await?;
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(edit_res.edits.len(), 2);
assert!(edit_res.edits[0]
@ -228,12 +233,15 @@ async fn test_edit_local_article() -> MyResult<()> {
async fn test_edit_remote_article() -> MyResult<()> {
let data = TestData::start().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?;
let beta_id_on_alpha = data.alpha.follow_instance(&data.beta.hostname).await?;
let beta_id_on_gamma = data.gamma.follow_instance(&data.beta.hostname).await?;
// create new article
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.beta, title.clone()).await?;
let create_res = data
.beta
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(&title, &create_res.article.title);
assert!(create_res.article.local);
@ -269,7 +277,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = edit_article(&data.alpha, &edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(2, edit_res.edits.len());
assert!(!edit_res.article.local);
@ -298,7 +306,10 @@ async fn test_local_edit_conflict() -> MyResult<()> {
// create new article
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.alpha, title.clone()).await?;
let create_res = data
.alpha
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(title, create_res.article.title);
assert!(create_res.article.local);
@ -309,7 +320,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None,
};
let edit_res = edit_article(&data.alpha, &edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(2, edit_res.edits.len());
@ -320,7 +331,9 @@ async fn test_local_edit_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = edit_article_with_conflict(&data.alpha, &edit_form)
let edit_res = data
.alpha
.edit_article_with_conflict(&edit_form)
.await?
.unwrap();
assert_eq!("<<<<<<< ours\nIpsum Lorem\n||||||| original\nsome\nexample\ntext\n=======\nLorem Ipsum\n>>>>>>> theirs\n", edit_res.three_way_merge);
@ -335,7 +348,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
previous_version_id: edit_res.previous_version_id,
resolve_conflict_id: Some(edit_res.id),
};
let edit_res = edit_article(&data.alpha, &edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.article.text);
let conflicts = get_conflicts(&data.alpha).await?;
@ -348,11 +361,14 @@ async fn test_local_edit_conflict() -> MyResult<()> {
async fn test_federated_edit_conflict() -> MyResult<()> {
let data = TestData::start().await;
let beta_id_on_alpha = follow_instance(&data.alpha, &data.beta.hostname).await?;
let beta_id_on_alpha = data.alpha.follow_instance(&data.beta.hostname).await?;
// create new article
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.beta, title.clone()).await?;
let create_res = data
.beta
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(title, create_res.article.title);
assert!(create_res.article.local);
@ -383,7 +399,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None,
};
let edit_res = edit_article(&data.alpha, &edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(2, edit_res.edits.len());
assert!(!edit_res.article.local);
@ -400,7 +416,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = edit_article(&data.gamma, &edit_form).await?;
let edit_res = data.gamma.edit_article(&edit_form).await?;
assert_ne!(edit_form.new_text, edit_res.article.text);
assert_eq!(1, edit_res.edits.len());
assert!(!edit_res.article.local);
@ -415,7 +431,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
previous_version_id: conflicts[0].previous_version_id.clone(),
resolve_conflict_id: Some(conflicts[0].id.clone()),
};
let edit_res = edit_article(&data.gamma, &edit_form).await?;
let edit_res = data.gamma.edit_article(&edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(3, edit_res.edits.len());
@ -431,7 +447,10 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
// create new article
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.alpha, title.clone()).await?;
let create_res = data
.alpha
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(title, create_res.article.title);
assert!(create_res.article.local);
@ -442,7 +461,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None,
};
let edit_res = edit_article(&data.alpha, &edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(2, edit_res.edits.len());
@ -453,7 +472,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = edit_article(&data.alpha, &edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await?;
let conflicts = get_conflicts(&data.alpha).await?;
assert_eq!(0, conflicts.len());
assert_eq!(3, edit_res.edits.len());
@ -468,7 +487,10 @@ async fn test_fork_article() -> MyResult<()> {
// create article
let title = "Manu_Chao".to_string();
let create_res = create_article(&data.alpha, title.clone()).await?;
let create_res = data
.alpha
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await?;
assert_eq!(title, create_res.article.title);
assert!(create_res.article.local);
@ -497,15 +519,14 @@ async fn test_fork_article() -> MyResult<()> {
assert_ne!(resolved_article.ap_id, forked_article.ap_id);
assert!(forked_article.local);
let beta_instance: InstanceView = get(&data.beta.hostname, "instance").await?;
let beta_instance = data.beta.get_local_instance().await?;
assert_eq!(forked_article.instance_id, beta_instance.instance.id);
// now search returns two articles for this title (original and forked)
let search_form = SearchArticleData {
query: title.clone(),
};
let search_res: Vec<DbArticle> =
get_query(&data.beta.hostname, "search", Some(search_form)).await?;
let search_res = data.beta.search(&search_form).await?;
assert_eq!(2, search_res.len());
data.stop()