1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2024-12-05 02:41:08 +00:00

Some improvements to error handling

This commit is contained in:
Felix Ableitner 2024-12-03 14:52:33 +01:00
parent ddad6e58a8
commit fc3d625a98
18 changed files with 313 additions and 261 deletions

View file

@ -31,7 +31,6 @@ unwrap_used = "deny"
# frontend and shared deps
[dependencies]
anyhow = "1.0.93"
leptos = "0.7.0"
leptos_meta = "0.7.0"
leptos_router = "0.7.0"
@ -99,6 +98,7 @@ tower-layer = { version = "0.3.3" }
reqwest = { version = "0.12.9", features = ["json", "cookies"] }
futures = "0.3.31"
env_logger = { version = "0.11.5", default-features = false }
anyhow = "1.0.93"
[dev-dependencies]
pretty_assertions = "1.4.1"

View file

@ -1,8 +1,10 @@
use crate::common::DbArticle;
use anyhow::{anyhow, Result};
use leptos::server_fn::error::ServerFnErrorErr;
pub fn can_edit_article(article: &DbArticle, is_admin: bool) -> Result<()> {
let err = anyhow!("Article is protected, only admins on origin instance can edit");
pub fn can_edit_article(article: &DbArticle, is_admin: bool) -> Result<(), ServerFnErrorErr> {
let err = ServerFnErrorErr::ServerError(
"Article is protected, only admins on origin instance can edit".to_string(),
);
if article.protected {
if !article.local {
return Err(err);

View file

@ -1,37 +1,11 @@
use crate::{
common::{
newtypes::{ArticleId, ConflictId},
utils::http_protocol_str,
ApiConflict,
ApproveArticleForm,
ArticleView,
CreateArticleForm,
DbArticle,
DbInstance,
DbPerson,
DeleteConflictForm,
EditArticleForm,
FollowInstance,
ForkArticleForm,
GetArticleForm,
GetInstance,
GetUserForm,
InstanceView,
ListArticlesForm,
LocalUserView,
LoginUserForm,
Notification,
ProtectArticleForm,
RegisterUserForm,
ResolveObject,
SearchArticleForm,
SiteView,
SuccessResponse,
},
frontend::error::MyResult,
use crate::common::{
newtypes::{ArticleId, ConflictId},
utils::http_protocol_str,
*,
};
use anyhow::anyhow;
use http::*;
use http::{Method, StatusCode};
use leptos::prelude::ServerFnError;
use log::error;
use serde::{Deserialize, Serialize};
use std::{fmt::Debug, sync::LazyLock};
use url::Url;
@ -79,24 +53,30 @@ impl ApiClient {
Self { hostname, ssl }
}
pub async fn get_article(&self, data: GetArticleForm) -> MyResult<ArticleView> {
pub async fn get_article(&self, data: GetArticleForm) -> Option<ArticleView> {
self.get("/api/v1/article", Some(data)).await
}
pub async fn list_articles(&self, data: ListArticlesForm) -> MyResult<Vec<DbArticle>> {
self.get("/api/v1/article/list", Some(data)).await
pub async fn list_articles(&self, data: ListArticlesForm) -> Option<Vec<DbArticle>> {
Some(self.get("/api/v1/article/list", Some(data)).await.unwrap())
}
pub async fn register(&self, register_form: RegisterUserForm) -> MyResult<LocalUserView> {
pub async fn register(
&self,
register_form: RegisterUserForm,
) -> Result<LocalUserView, ServerFnError> {
self.post("/api/v1/account/register", Some(&register_form))
.await
}
pub async fn login(&self, login_form: LoginUserForm) -> MyResult<LocalUserView> {
pub async fn login(&self, login_form: LoginUserForm) -> Result<LocalUserView, ServerFnError> {
self.post("/api/v1/account/login", Some(&login_form)).await
}
pub async fn create_article(&self, data: &CreateArticleForm) -> MyResult<ArticleView> {
pub async fn create_article(
&self,
data: &CreateArticleForm,
) -> Result<ArticleView, ServerFnError> {
self.send(Method::POST, "/api/v1/article", Some(&data))
.await
}
@ -104,13 +84,17 @@ impl ApiClient {
pub async fn edit_article_with_conflict(
&self,
edit_form: &EditArticleForm,
) -> MyResult<Option<ApiConflict>> {
) -> Result<Option<ApiConflict>, ServerFnError> {
self.send(Method::PATCH, "/api/v1/article", Some(&edit_form))
.await
}
pub async fn edit_article(&self, edit_form: &EditArticleForm) -> MyResult<ArticleView> {
let edit_res = self.edit_article_with_conflict(edit_form).await?;
pub async fn edit_article(&self, edit_form: &EditArticleForm) -> Option<ArticleView> {
let edit_res = self
.edit_article_with_conflict(edit_form)
.await
.map_err(|e| error!("edit failed {e}"))
.ok()?;
assert!(edit_res.is_none());
self.get_article(GetArticleForm {
@ -121,53 +105,58 @@ impl ApiClient {
.await
}
pub async fn notifications_list(&self) -> MyResult<Vec<Notification>> {
pub async fn notifications_list(&self) -> Option<Vec<Notification>> {
self.get("/api/v1/user/notifications/list", None::<()>)
.await
}
pub async fn notifications_count(&self) -> MyResult<usize> {
pub async fn notifications_count(&self) -> Option<usize> {
self.get("/api/v1/user/notifications/count", None::<()>)
.await
}
pub async fn approve_article(&self, article_id: ArticleId, approve: bool) -> MyResult<()> {
pub async fn approve_article(&self, article_id: ArticleId, approve: bool) -> Option<()> {
let form = ApproveArticleForm {
article_id,
approve,
};
self.post("/api/v1/article/approve", Some(&form)).await
result_to_option(self.post("/api/v1/article/approve", Some(&form)).await)
}
pub async fn delete_conflict(&self, conflict_id: ConflictId) -> MyResult<()> {
pub async fn delete_conflict(&self, conflict_id: ConflictId) -> Option<()> {
let form = DeleteConflictForm { conflict_id };
self.send(Method::DELETE, "/api/v1/conflict", Some(form))
result_to_option(
self.send(Method::DELETE, "/api/v1/conflict", Some(form))
.await,
)
}
pub async fn search(
&self,
search_form: &SearchArticleForm,
) -> Result<Vec<DbArticle>, ServerFnError> {
self.send(Method::GET, "/api/v1/search", Some(search_form))
.await
}
pub async fn search(&self, search_form: &SearchArticleForm) -> MyResult<Vec<DbArticle>> {
self.get("/api/v1/search", Some(search_form)).await
}
pub async fn get_local_instance(&self) -> MyResult<InstanceView> {
pub async fn get_local_instance(&self) -> Option<InstanceView> {
self.get("/api/v1/instance", None::<i32>).await
}
pub async fn get_instance(&self, get_form: &GetInstance) -> MyResult<InstanceView> {
pub async fn get_instance(&self, get_form: &GetInstance) -> Option<InstanceView> {
self.get("/api/v1/instance", Some(&get_form)).await
}
pub async fn list_instances(&self) -> MyResult<Vec<DbInstance>> {
pub async fn list_instances(&self) -> Option<Vec<DbInstance>> {
self.get("/api/v1/instance/list", None::<i32>).await
}
pub async fn follow_instance_with_resolve(
&self,
follow_instance: &str,
) -> MyResult<DbInstance> {
pub async fn follow_instance_with_resolve(&self, follow_instance: &str) -> Option<DbInstance> {
// fetch beta instance on alpha
let resolve_form = ResolveObject {
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))?,
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))
.map_err(|e| error!("invalid url {e}"))
.ok()?,
};
let instance_resolved: DbInstance = self
.get("/api/v1/instance/resolve", Some(resolve_form))
@ -178,54 +167,63 @@ impl ApiClient {
id: instance_resolved.id,
};
self.follow_instance(follow_form).await?;
Ok(instance_resolved)
Some(instance_resolved)
}
pub async fn follow_instance(&self, follow_form: FollowInstance) -> MyResult<SuccessResponse> {
self.post("/api/v1/instance/follow", Some(follow_form))
.await
pub async fn follow_instance(&self, follow_form: FollowInstance) -> Option<SuccessResponse> {
result_to_option(
self.post("/api/v1/instance/follow", Some(follow_form))
.await,
)
}
pub async fn site(&self) -> MyResult<SiteView> {
pub async fn site(&self) -> Option<SiteView> {
self.get("/api/v1/site", None::<()>).await
}
pub async fn logout(&self) -> MyResult<SuccessResponse> {
self.post("/api/v1/account/logout", None::<()>).await
pub async fn logout(&self) -> Option<SuccessResponse> {
result_to_option(self.post("/api/v1/account/logout", None::<()>).await)
}
pub async fn fork_article(&self, form: &ForkArticleForm) -> MyResult<ArticleView> {
Ok(self.post("/api/v1/article/fork", Some(form)).await.unwrap())
pub async fn fork_article(&self, form: &ForkArticleForm) -> Result<ArticleView, ServerFnError> {
self.post("/api/v1/article/fork", Some(form)).await
}
pub async fn protect_article(&self, params: &ProtectArticleForm) -> MyResult<DbArticle> {
pub async fn protect_article(
&self,
params: &ProtectArticleForm,
) -> Result<DbArticle, ServerFnError> {
self.post("/api/v1/article/protect", Some(params)).await
}
pub async fn resolve_article(&self, id: Url) -> MyResult<ArticleView> {
pub async fn resolve_article(&self, id: Url) -> Result<ArticleView, ServerFnError> {
let resolve_object = ResolveObject { id };
self.get("/api/v1/article/resolve", Some(resolve_object))
self.send(Method::GET, "/api/v1/article/resolve", Some(resolve_object))
.await
}
pub async fn resolve_instance(&self, id: Url) -> MyResult<DbInstance> {
pub async fn resolve_instance(&self, id: Url) -> Result<DbInstance, ServerFnError> {
let resolve_object = ResolveObject { id };
self.get("/api/v1/instance/resolve", Some(resolve_object))
.await
self.send(
Method::GET,
"/api/v1/instance/resolve",
Some(resolve_object),
)
.await
}
pub async fn get_user(&self, data: GetUserForm) -> MyResult<DbPerson> {
pub async fn get_user(&self, data: GetUserForm) -> Option<DbPerson> {
self.get("/api/v1/user", Some(data)).await
}
async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> MyResult<T>
async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> Option<T>
where
T: for<'de> Deserialize<'de>,
R: Serialize + Debug,
{
self.send(Method::GET, endpoint, query).await
result_to_option(self.send(Method::GET, endpoint, query).await)
}
async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> MyResult<T>
async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError>
where
T: for<'de> Deserialize<'de>,
R: Serialize + Debug,
@ -234,7 +232,12 @@ impl ApiClient {
}
#[cfg(feature = "ssr")]
async fn send<P, T>(&self, method: Method, path: &str, params: Option<P>) -> MyResult<T>
async fn send<P, T>(
&self,
method: Method,
path: &str,
params: Option<P>,
) -> Result<T, ServerFnError>
where
P: Serialize + Debug,
T: for<'de> Deserialize<'de>,
@ -266,7 +269,7 @@ impl ApiClient {
method: Method,
path: &'a str,
params: Option<P>,
) -> impl std::future::Future<Output = MyResult<T>> + Send + 'a
) -> impl std::future::Future<Output = Result<T, ServerFnError>> + Send + 'a
where
P: Serialize + Debug + 'a,
T: for<'de> Deserialize<'de>,
@ -309,21 +312,22 @@ impl ApiClient {
builder.build()
}
.unwrap();
let res = req.send().await.unwrap();
let res = req.send().await?;
let status = res.status();
let text = res.text().await.unwrap();
let text = res.text().await?;
Self::response(status, text)
})
}
fn response<T>(status: u16, text: String) -> MyResult<T>
fn response<T>(status: u16, text: String) -> Result<T, ServerFnError>
where
T: for<'de> Deserialize<'de>,
{
let json = serde_json::from_str(&text)?;
if status == StatusCode::OK {
Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?)
Ok(json)
} else {
Err(anyhow!("API error: {text}").into())
Err(ServerFnError::Response(format!("API error: {text}")))
}
}
@ -332,3 +336,13 @@ impl ApiClient {
format!("{protocol}://{}{path}", &self.hostname)
}
}
fn result_to_option<T>(val: Result<T, ServerFnError>) -> Option<T> {
match val {
Ok(v) => Some(v),
Err(e) => {
error!("API error: {e}");
None
}
}
}

View file

@ -47,15 +47,22 @@ pub fn is_admin() -> bool {
}
pub trait DefaultResource<T> {
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O;
fn get_default(&self) -> T;
}
impl<T: Default + Send + Sync> DefaultResource<T> for Resource<T> {
impl<T: Default + Send + Sync + Clone> DefaultResource<T> for Resource<T> {
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O {
self.with(|x| match x {
Some(x) => f(x),
None => f(&T::default()),
})
}
fn get_default(&self) -> T {
match self.get() {
Some(x) => x.clone(),
None => T::default(),
}
}
}
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@ -79,7 +86,6 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
pub fn App() -> impl IntoView {
provide_meta_context();
// TODO: should Resource::new() but then things break
let site_resource = Resource::new(|| (), |_| async move { CLIENT.site().await.unwrap() });
provide_context(site_resource);

View file

@ -2,10 +2,7 @@ use katex;
use markdown_it::{
parser::inline::{InlineRule, InlineState},
plugins::cmark::block::{heading::ATXHeading, lheading::SetextHeader},
MarkdownIt,
Node,
NodeValue,
Renderer,
MarkdownIt, Node, NodeValue, Renderer,
};
use once_cell::sync::OnceCell;
@ -102,9 +99,9 @@ impl NodeValue for MathEquation {
.throw_on_error(false)
.display_mode(self.display_mode)
.build()
.unwrap();
let katex_equation = katex::render_with_opts(&self.equation, opts).unwrap();
fmt.text_raw(&katex_equation)
.ok();
let katex_equation = opts.and_then(|o| katex::render_with_opts(&self.equation, o).ok());
fmt.text_raw(katex_equation.as_ref().unwrap_or(&self.equation))
}
}
@ -153,6 +150,7 @@ fn test_markdown_article_link() {
}
#[test]
#[expect(clippy::unwrap_used)]
fn test_markdown_equation_katex() {
let parser = markdown_parser();
let rendered = parser

View file

@ -6,7 +6,6 @@ pub mod api;
pub mod app;
mod components;
pub mod dark_mode;
pub mod error;
pub mod markdown;
pub mod pages;

View file

@ -29,7 +29,7 @@ pub fn ArticleActions() -> impl IntoView {
match result {
Ok(res) => set_fork_response.set(Some(res.article)),
Err(err) => {
set_error.update(|e| *e = Some(err.0.to_string()));
set_error.update(|e| *e = Some(err.to_string()));
}
}
}
@ -45,7 +45,7 @@ pub fn ArticleActions() -> impl IntoView {
match result {
Ok(_res) => article.refetch(),
Err(err) => {
set_error.update(|e| *e = Some(err.0.to_string()));
set_error.update(|e| *e = Some(err.to_string()));
}
}
}

View file

@ -44,7 +44,7 @@ pub fn CreateArticle() -> impl IntoView {
set_create_error.update(|e| *e = None);
}
Err(err) => {
let msg = err.0.to_string();
let msg = err.to_string();
log::warn!("Unable to create: {msg}");
set_create_error.update(|e| *e = Some(msg));
}

View file

@ -100,7 +100,7 @@ pub fn EditArticle() -> impl IntoView {
set_edit_response.update(|v| *v = EditResponse::Success);
}
Err(err) => {
let msg = err.0.to_string();
let msg = err.to_string();
log::warn!("Unable to edit: {msg}");
set_edit_error.update(|e| *e = Some(msg));
}

View file

@ -1,6 +1,9 @@
use crate::{
common::ListArticlesForm,
frontend::{api::CLIENT, article_link, article_title, components::connect::ConnectView},
frontend::{
api::CLIENT, app::DefaultResource, article_link, article_title,
components::connect::ConnectView,
},
};
use leptos::prelude::*;
@ -16,7 +19,6 @@ pub fn ListArticles() -> impl IntoView {
instance_id: None,
})
.await
.unwrap()
},
);
let only_local_class = Resource::new(
@ -64,27 +66,27 @@ pub fn ListArticles() -> impl IntoView {
/>
</div>
<Show
when=move || { articles.get().unwrap_or_default().len() > 1 || only_local.get() }
when=move || {
articles.get_default().unwrap_or_default().len() > 1 || only_local.get()
}
fallback=move || view! { <ConnectView res=articles /> }
>
<ul class="list-none my-4">
{move || {
articles
.get()
.map(|a| {
a.into_iter()
.map(|a| {
view! {
<li>
<a class="link text-lg" href=article_link(&a)>
{article_title(&a)}
</a>
</li>
}
})
.collect::<Vec<_>>()
})
}}
<For
each=move || articles.get_default().unwrap_or_default()
key=|article| article.id
let:article
>
{
view! {
<li>
<a class="link text-lg" href=article_link(&article)>
{article_title(&article)}
</a>
</li>
}
}
</For>
</ul>
</Show>

View file

@ -26,7 +26,7 @@ pub fn Login() -> impl IntoView {
set_login_error.update(|e| *e = None);
}
Err(err) => {
let msg = err.0.to_string();
let msg = err.to_string();
log::warn!("Unable to login: {msg}");
set_login_error.update(|e| *e = Some(msg));
}

View file

@ -8,7 +8,7 @@ use leptos::prelude::*;
pub fn Notifications() -> impl IntoView {
let notifications = Resource::new(
move || {},
|_| async move { CLIENT.notifications_list().await.unwrap() },
|_| async move { CLIENT.notifications_list().await.unwrap_or_default() },
);
view! {

View file

@ -1,6 +1,6 @@
use crate::{
common::{LocalUserView, RegisterUserForm},
frontend::{api::CLIENT, app::site, components::credentials::*, error::MyResult},
common::RegisterUserForm,
frontend::{api::CLIENT, app::site, components::credentials::*},
};
use leptos::prelude::*;
use log::info;
@ -18,7 +18,7 @@ pub fn Register() -> impl IntoView {
info!("Try to register new account for {}", credentials.username);
async move {
set_wait_for_response.update(|w| *w = true);
let result: MyResult<LocalUserView> = CLIENT.register(credentials).await;
let result = CLIENT.register(credentials).await;
set_wait_for_response.update(|w| *w = false);
match result {
Ok(_res) => {
@ -27,7 +27,7 @@ pub fn Register() -> impl IntoView {
set_register_error.update(|e| *e = None);
}
Err(err) => {
let msg = err.0.to_string();
let msg = err.to_string();
log::warn!("Unable to register new account: {msg}");
set_register_error.update(|e| *e = Some(msg));
}

View file

@ -22,7 +22,7 @@ impl SearchResults {
#[component]
pub fn Search() -> impl IntoView {
let params = use_query_map();
let query = move || params.get().get("query").clone().unwrap();
let query = move || params.get().get("query").clone().unwrap_or_default();
let (error, set_error) = signal(None::<String>);
let search_results = Resource::new(query, move |query| async move {
set_error.set(None);
@ -33,18 +33,18 @@ pub fn Search() -> impl IntoView {
match search.await {
Ok(mut a) => search_results.articles.append(&mut a),
Err(e) => set_error.set(Some(e.0.to_string())),
Err(e) => set_error.set(Some(e.to_string())),
}
// If its a valid url, also attempt to resolve as federation object
if let Ok(url) = url {
match CLIENT.resolve_article(url.clone()).await {
Ok(a) => search_results.articles.push(a.article),
Err(e) => set_error.set(Some(e.0.to_string())),
Err(e) => set_error.set(Some(e.to_string())),
}
match CLIENT.resolve_instance(url).await {
Ok(a) => search_results.instance = Some(a),
Err(e) => set_error.set(Some(e.0.to_string())),
Err(e) => set_error.set(Some(e.to_string())),
}
}
search_results

View file

@ -1,6 +1,6 @@
use crate::{
common::{DbPerson, GetUserForm},
frontend::{api::CLIENT, user_title},
frontend::{api::CLIENT, app::DefaultResource, user_title},
};
use leptos::prelude::*;
use leptos_router::hooks::use_params_map;
@ -8,7 +8,7 @@ use leptos_router::hooks::use_params_map;
#[component]
pub fn UserProfile() -> impl IntoView {
let params = use_params_map();
let name = move || params.get().get("name").clone().unwrap();
let name = move || params.get().get("name").clone().unwrap_or_default();
let (error, set_error) = signal(None::<String>);
let user_profile = Resource::new(name, move |mut name| async move {
set_error.set(None);
@ -18,7 +18,7 @@ pub fn UserProfile() -> impl IntoView {
domain = Some(domain_.to_string());
}
let params = GetUserForm { name, domain };
CLIENT.get_user(params).await.unwrap()
CLIENT.get_user(params).await
});
view! {
@ -35,7 +35,7 @@ pub fn UserProfile() -> impl IntoView {
}>
{move || {
user_profile
.get()
.get_default()
.map(|person: DbPerson| {
view! {
<h1 class="text-4xl font-bold font-serif my-6 grow flex-auto">

View file

@ -1,5 +1,5 @@
#[cfg(feature = "ssr")]
pub mod backend;
pub mod common;
#[allow(clippy::unwrap_used)]
#[expect(clippy::unwrap_used)]
pub mod frontend;

View file

@ -1,12 +1,13 @@
#![allow(clippy::unwrap_used)]
#![expect(clippy::unwrap_used)]
use anyhow::Result;
use ibis::{
backend::{
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
start,
},
common::{Options, RegisterUserForm},
frontend::{api::ApiClient, error::MyResult},
frontend::api::ApiClient,
};
use reqwest::ClientBuilder;
use std::{
@ -75,7 +76,7 @@ impl TestData {
Self { alpha, beta, gamma }
}
pub fn stop(self) -> MyResult<()> {
pub fn stop(self) -> Result<()> {
for j in [self.alpha.stop(), self.beta.stop(), self.gamma.stop()] {
j.join().unwrap();
}

View file

@ -1,32 +1,20 @@
#![allow(clippy::unwrap_used)]
#![expect(clippy::unwrap_used)]
mod common;
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
use ibis::{
common::{
utils::extract_domain,
ArticleView,
CreateArticleForm,
EditArticleForm,
ForkArticleForm,
GetArticleForm,
GetUserForm,
ListArticlesForm,
LoginUserForm,
Notification,
ProtectArticleForm,
RegisterUserForm,
SearchArticleForm,
},
frontend::error::MyResult,
use anyhow::Result;
use ibis::common::{
utils::extract_domain, ArticleView, CreateArticleForm, EditArticleForm, ForkArticleForm,
GetArticleForm, GetUserForm, ListArticlesForm, LoginUserForm, Notification, ProtectArticleForm,
RegisterUserForm, SearchArticleForm,
};
use pretty_assertions::{assert_eq, assert_ne};
use retry_future::{LinearRetryStrategy, RetryFuture, RetryPolicy};
use url::Url;
#[tokio::test]
async fn test_create_read_and_edit_local_article() -> MyResult<()> {
async fn test_create_read_and_edit_local_article() -> Result<()> {
let data = TestData::start(false).await;
// create article
@ -35,7 +23,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local);
@ -45,14 +33,18 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
domain: None,
id: None,
};
let get_res = data.alpha.get_article(get_article_data.clone()).await?;
let get_res = data
.alpha
.get_article(get_article_data.clone())
.await
.unwrap();
assert_eq!(create_form.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 = data.beta.get_article(get_article_data.clone()).await;
assert!(not_found.is_err());
assert!(not_found.is_none());
// edit article
let edit_form = EditArticleForm {
@ -62,7 +54,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = data.alpha.edit_article(&edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await.unwrap();
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(2, edit_res.edits.len());
assert_eq!(edit_form.summary, edit_res.edits[1].edit.summary);
@ -70,7 +62,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
let search_form = SearchArticleForm {
query: create_form.title.clone(),
};
let search_res = data.alpha.search(&search_form).await?;
let search_res = data.alpha.search(&search_form).await.unwrap();
assert_eq!(1, search_res.len());
assert_eq!(edit_res.article, search_res[0]);
@ -80,7 +72,8 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
only_local: Some(false),
instance_id: None,
})
.await?;
.await
.unwrap();
assert_eq!(2, list_articles.len());
assert_eq!(edit_res.article, list_articles[0]);
@ -88,7 +81,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
}
#[tokio::test]
async fn test_create_duplicate_article() -> MyResult<()> {
async fn test_create_duplicate_article() -> Result<()> {
let data = TestData::start(false).await;
// create article
@ -97,7 +90,7 @@ async fn test_create_duplicate_article() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local);
@ -108,25 +101,26 @@ async fn test_create_duplicate_article() -> MyResult<()> {
}
#[tokio::test]
async fn test_follow_instance() -> MyResult<()> {
async fn test_follow_instance() -> Result<()> {
let data = TestData::start(false).await;
// check initial state
let alpha_user = data.alpha.site().await?.my_profile.unwrap();
let alpha_user = data.alpha.site().await.unwrap().my_profile.unwrap();
assert_eq!(0, alpha_user.following.len());
let beta_instance = data.beta.get_local_instance().await?;
let beta_instance = data.beta.get_local_instance().await.unwrap();
assert_eq!(0, beta_instance.followers.len());
data.alpha
.follow_instance_with_resolve(&data.beta.hostname)
.await?;
.await
.unwrap();
// check that follow was federated
let alpha_user = data.alpha.site().await?.my_profile.unwrap();
let alpha_user = data.alpha.site().await.unwrap().my_profile.unwrap();
assert_eq!(1, alpha_user.following.len());
assert_eq!(beta_instance.instance.ap_id, alpha_user.following[0].ap_id);
let beta_instance = data.beta.get_local_instance().await?;
let beta_instance = data.beta.get_local_instance().await.unwrap();
assert_eq!(1, beta_instance.followers.len());
assert_eq!(alpha_user.person.ap_id, beta_instance.followers[0].ap_id);
@ -134,7 +128,7 @@ async fn test_follow_instance() -> MyResult<()> {
}
#[tokio::test]
async fn test_synchronize_articles() -> MyResult<()> {
async fn test_synchronize_articles() -> Result<()> {
let data = TestData::start(false).await;
// create article on alpha
@ -143,7 +137,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
assert_eq!(create_form.title, create_res.article.title);
assert_eq!(1, create_res.edits.len());
assert!(create_res.article.local);
@ -156,13 +150,14 @@ async fn test_synchronize_articles() -> MyResult<()> {
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
data.alpha.edit_article(&edit_form).await?;
data.alpha.edit_article(&edit_form).await.unwrap();
// fetch alpha instance on beta, articles are also fetched automatically
let instance = data
.beta
.resolve_instance(Url::parse(&format!("http://{}", &data.alpha.hostname))?)
.await?;
.await
.unwrap();
let get_article_data = GetArticleForm {
title: Some(create_res.article.title.clone()),
@ -171,7 +166,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
// 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());
assert!(get_res.is_none());
// get the article with instance id and compare
let get_res = RetryFuture::new(
@ -183,9 +178,9 @@ async fn test_synchronize_articles() -> MyResult<()> {
};
let res = data.beta.get_article(get_article_data).await;
match res {
Err(_) => Err(RetryPolicy::<String>::Retry(None)),
Ok(a) if a.edits.len() < 2 => Err(RetryPolicy::Retry(None)),
Ok(a) => Ok(a),
None => Err(RetryPolicy::<String>::Retry(None)),
Some(a) if a.edits.len() < 2 => Err(RetryPolicy::Retry(None)),
Some(a) => Ok(a),
}
},
LinearRetryStrategy::new(),
@ -201,13 +196,14 @@ async fn test_synchronize_articles() -> MyResult<()> {
}
#[tokio::test]
async fn test_edit_local_article() -> MyResult<()> {
async fn test_edit_local_article() -> Result<()> {
let data = TestData::start(false).await;
let beta_instance = data
.alpha
.follow_instance_with_resolve(&data.beta.hostname)
.await?;
.await
.unwrap();
// create new article
let create_form = CreateArticleForm {
@ -215,7 +211,7 @@ async fn test_edit_local_article() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.beta.create_article(&create_form).await?;
let create_res = data.beta.create_article(&create_form).await.unwrap();
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local);
@ -225,7 +221,11 @@ async fn test_edit_local_article() -> MyResult<()> {
domain: Some(beta_instance.domain),
id: None,
};
let get_res = data.alpha.get_article(get_article_data.clone()).await?;
let get_res = data
.alpha
.get_article(get_article_data.clone())
.await
.unwrap();
assert_eq!(create_res.article.title, get_res.article.title);
assert_eq!(1, get_res.edits.len());
assert!(!get_res.article.local);
@ -239,7 +239,7 @@ async fn test_edit_local_article() -> MyResult<()> {
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = data.beta.edit_article(&edit_form).await?;
let edit_res = data.beta.edit_article(&edit_form).await.unwrap();
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(edit_res.edits.len(), 2);
assert!(edit_res.edits[0]
@ -249,7 +249,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 = data.alpha.get_article(get_article_data).await?;
let get_res = data.alpha.get_article(get_article_data).await.unwrap();
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);
@ -258,17 +258,19 @@ async fn test_edit_local_article() -> MyResult<()> {
}
#[tokio::test]
async fn test_edit_remote_article() -> MyResult<()> {
async fn test_edit_remote_article() -> Result<()> {
let data = TestData::start(false).await;
let beta_id_on_alpha = data
.alpha
.follow_instance_with_resolve(&data.beta.hostname)
.await?;
.await
.unwrap();
let beta_id_on_gamma = data
.gamma
.follow_instance_with_resolve(&data.beta.hostname)
.await?;
.await
.unwrap();
// create new article
let create_form = CreateArticleForm {
@ -276,7 +278,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.beta.create_article(&create_form).await?;
let create_res = data.beta.create_article(&create_form).await.unwrap();
assert_eq!(&create_form.title, &create_res.article.title);
assert!(create_res.article.local);
@ -289,7 +291,8 @@ async fn test_edit_remote_article() -> MyResult<()> {
let get_res = data
.alpha
.get_article(get_article_data_alpha.clone())
.await?;
.await
.unwrap();
assert_eq!(create_res.article.title, get_res.article.title);
assert_eq!(1, get_res.edits.len());
assert!(!get_res.article.local);
@ -302,7 +305,8 @@ async fn test_edit_remote_article() -> MyResult<()> {
let get_res = data
.gamma
.get_article(get_article_data_gamma.clone())
.await?;
.await
.unwrap();
assert_eq!(create_res.article.title, get_res.article.title);
assert_eq!(create_res.article.text, get_res.article.text);
@ -313,7 +317,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = data.alpha.edit_article(&edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await.unwrap();
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(2, edit_res.edits.len());
assert!(!edit_res.article.local);
@ -324,12 +328,20 @@ 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 = data.alpha.get_article(get_article_data_alpha).await?;
let get_res = data
.alpha
.get_article(get_article_data_alpha)
.await
.unwrap();
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 = data.gamma.get_article(get_article_data_gamma).await?;
let get_res = data
.gamma
.get_article(get_article_data_gamma)
.await
.unwrap();
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);
@ -338,7 +350,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
}
#[tokio::test]
async fn test_local_edit_conflict() -> MyResult<()> {
async fn test_local_edit_conflict() -> Result<()> {
let data = TestData::start(false).await;
// create new article
@ -347,7 +359,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local);
@ -359,7 +371,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None,
};
let edit_res = data.alpha.edit_article(&edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await.unwrap();
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(2, edit_res.edits.len());
@ -374,11 +386,12 @@ async fn test_local_edit_conflict() -> MyResult<()> {
let edit_res = data
.alpha
.edit_article_with_conflict(&edit_form)
.await?
.await
.unwrap()
.unwrap();
assert_eq!("<<<<<<< ours\nIpsum Lorem\n||||||| original\nsome\nexample\ntext\n=======\nLorem Ipsum\n>>>>>>> theirs\n", edit_res.three_way_merge);
let notifications = data.alpha.notifications_list().await?;
let notifications = data.alpha.notifications_list().await.unwrap();
assert_eq!(1, notifications.len());
let Notification::EditConflict(conflict) = &notifications[0] else {
panic!()
@ -392,22 +405,23 @@ 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 = data.alpha.edit_article(&edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await.unwrap();
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(0, data.alpha.notifications_count().await?);
assert_eq!(0, data.alpha.notifications_count().await.unwrap());
data.stop()
}
#[tokio::test]
async fn test_federated_edit_conflict() -> MyResult<()> {
async fn test_federated_edit_conflict() -> Result<()> {
let data = TestData::start(false).await;
let beta_id_on_alpha = data
.alpha
.follow_instance_with_resolve(&data.beta.hostname)
.await?;
.await
.unwrap();
// create new article
let create_form = CreateArticleForm {
@ -415,7 +429,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.beta.create_article(&create_form).await?;
let create_res = data.beta.create_article(&create_form).await.unwrap();
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local);
@ -423,7 +437,8 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
let resolve_res: ArticleView = data
.gamma
.resolve_article(create_res.article.ap_id.inner().clone())
.await?;
.await
.unwrap();
assert_eq!(create_res.article.text, resolve_res.article.text);
// alpha edits article
@ -432,7 +447,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
domain: Some(beta_id_on_alpha.domain),
id: None,
};
let get_res = data.alpha.get_article(get_article_data).await?;
let get_res = data.alpha.get_article(get_article_data).await.unwrap();
assert_eq!(&create_res.edits.len(), &get_res.edits.len());
assert_eq!(&create_res.edits[0].edit.hash, &get_res.edits[0].edit.hash);
let edit_form = EditArticleForm {
@ -442,7 +457,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None,
};
let edit_res = data.alpha.edit_article(&edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await.unwrap();
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(2, edit_res.edits.len());
assert!(!edit_res.article.local);
@ -461,13 +476,13 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = data.gamma.edit_article(&edit_form).await?;
let edit_res = data.gamma.edit_article(&edit_form).await.unwrap();
assert_ne!(edit_form.new_text, edit_res.article.text);
assert_eq!(1, edit_res.edits.len());
assert!(!edit_res.article.local);
assert_eq!(1, data.gamma.notifications_count().await?);
let notifications = data.gamma.notifications_list().await?;
assert_eq!(1, data.gamma.notifications_count().await.unwrap());
let notifications = data.gamma.notifications_list().await.unwrap();
assert_eq!(1, notifications.len());
let Notification::EditConflict(conflict) = &notifications[0] else {
panic!()
@ -481,19 +496,19 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
previous_version_id: conflict.previous_version_id.clone(),
resolve_conflict_id: Some(conflict.id),
};
let edit_res = data.gamma.edit_article(&edit_form).await?;
let edit_res = data.gamma.edit_article(&edit_form).await.unwrap();
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(3, edit_res.edits.len());
assert_eq!(0, data.gamma.notifications_count().await?);
let notifications = data.gamma.notifications_list().await?;
assert_eq!(0, data.gamma.notifications_count().await.unwrap());
let notifications = data.gamma.notifications_list().await.unwrap();
assert_eq!(0, notifications.len());
data.stop()
}
#[tokio::test]
async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
async fn test_overlapping_edits_no_conflict() -> Result<()> {
let data = TestData::start(false).await;
// create new article
@ -502,7 +517,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local);
@ -514,7 +529,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None,
};
let edit_res = data.alpha.edit_article(&edit_form).await?;
let edit_res = data.alpha.edit_article(&edit_form).await.unwrap();
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(2, edit_res.edits.len());
@ -526,8 +541,8 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(0, data.alpha.notifications_count().await?);
let edit_res = data.alpha.edit_article(&edit_form).await.unwrap();
assert_eq!(0, data.alpha.notifications_count().await.unwrap());
assert_eq!(3, edit_res.edits.len());
assert_eq!("my\nexample\narticle\n", edit_res.article.text);
@ -535,7 +550,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
}
#[tokio::test]
async fn test_fork_article() -> MyResult<()> {
async fn test_fork_article() -> Result<()> {
let data = TestData::start(false).await;
// create article
@ -544,7 +559,7 @@ async fn test_fork_article() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local);
@ -552,7 +567,8 @@ async fn test_fork_article() -> MyResult<()> {
let resolve_res = data
.beta
.resolve_article(create_res.article.ap_id.into_inner())
.await?;
.await
.unwrap();
let resolved_article = resolve_res.article;
assert_eq!(create_res.edits.len(), resolve_res.edits.len());
@ -561,7 +577,7 @@ async fn test_fork_article() -> MyResult<()> {
article_id: resolved_article.id,
new_title: resolved_article.title.clone(),
};
let fork_res = data.beta.fork_article(&fork_form).await?;
let fork_res = data.beta.fork_article(&fork_form).await.unwrap();
let forked_article = fork_res.article;
assert_eq!(resolved_article.title, forked_article.title);
assert_eq!(resolved_article.text, forked_article.text);
@ -573,21 +589,21 @@ async fn test_fork_article() -> MyResult<()> {
assert_ne!(resolved_article.ap_id, forked_article.ap_id);
assert!(forked_article.local);
let beta_instance = data.beta.get_local_instance().await?;
let beta_instance = data.beta.get_local_instance().await.unwrap();
assert_eq!(forked_article.instance_id, beta_instance.instance.id);
// now search returns two articles for this title (original and forked)
let search_form = SearchArticleForm {
query: create_form.title.clone(),
};
let search_res = data.beta.search(&search_form).await?;
let search_res = data.beta.search(&search_form).await.unwrap();
assert_eq!(2, search_res.len());
data.stop()
}
#[tokio::test]
async fn test_user_registration_login() -> MyResult<()> {
async fn test_user_registration_login() -> Result<()> {
let data = TestData::start(false).await;
let username = "my_user";
let password = "hunter2";
@ -595,7 +611,7 @@ async fn test_user_registration_login() -> MyResult<()> {
username: username.to_string(),
password: password.to_string(),
};
data.alpha.register(register_data).await?;
data.alpha.register(register_data).await.unwrap();
let login_data = LoginUserForm {
username: username.to_string(),
@ -608,21 +624,21 @@ async fn test_user_registration_login() -> MyResult<()> {
username: username.to_string(),
password: password.to_string(),
};
data.alpha.login(login_data).await?;
data.alpha.login(login_data).await.unwrap();
let my_profile = data.alpha.site().await?.my_profile.unwrap();
let my_profile = data.alpha.site().await.unwrap().my_profile.unwrap();
assert_eq!(username, my_profile.person.username);
data.alpha.logout().await?;
data.alpha.logout().await.unwrap();
let my_profile_after_logout = data.alpha.site().await?.my_profile;
let my_profile_after_logout = data.alpha.site().await.unwrap().my_profile;
assert!(my_profile_after_logout.is_none());
data.stop()
}
#[tokio::test]
async fn test_user_profile() -> MyResult<()> {
async fn test_user_profile() -> Result<()> {
let data = TestData::start(false).await;
// Create an article and federate it, in order to federate the user who created it
@ -631,18 +647,29 @@ async fn test_user_profile() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
data.beta
.resolve_article(create_res.article.ap_id.into_inner())
.await?;
let domain = extract_domain(&data.alpha.site().await?.my_profile.unwrap().person.ap_id);
.await
.unwrap();
let domain = extract_domain(
&data
.alpha
.site()
.await
.unwrap()
.my_profile
.unwrap()
.person
.ap_id,
);
// Now we can fetch the remote user from local api
let params = GetUserForm {
name: "alpha".to_string(),
domain: Some(domain),
};
let user = data.beta.get_user(params).await?;
let user = data.beta.get_user(params).await.unwrap();
assert_eq!("alpha", user.username);
assert!(!user.local);
@ -650,7 +677,7 @@ async fn test_user_profile() -> MyResult<()> {
}
#[tokio::test]
async fn test_lock_article() -> MyResult<()> {
async fn test_lock_article() -> Result<()> {
let data = TestData::start(false).await;
// create article
@ -659,7 +686,7 @@ async fn test_lock_article() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
assert!(!create_res.article.protected);
// lock from normal user fails
@ -675,14 +702,15 @@ async fn test_lock_article() -> MyResult<()> {
username: "ibis".to_string(),
password: "ibis".to_string(),
};
data.alpha.login(form).await?;
let lock_res = data.alpha.protect_article(&lock_form).await?;
data.alpha.login(form).await.unwrap();
let lock_res = data.alpha.protect_article(&lock_form).await.unwrap();
assert!(lock_res.protected);
let resolve_res: ArticleView = data
.gamma
.resolve_article(create_res.article.ap_id.inner().clone())
.await?;
.await
.unwrap();
let edit_form = EditArticleForm {
article_id: resolve_res.article.id,
new_text: "test".to_string(),
@ -691,35 +719,37 @@ async fn test_lock_article() -> MyResult<()> {
resolve_conflict_id: None,
};
let edit_res = data.gamma.edit_article(&edit_form).await;
assert!(edit_res.is_err());
assert!(edit_res.is_none());
data.stop()
}
#[tokio::test]
async fn test_synchronize_instances() -> MyResult<()> {
async fn test_synchronize_instances() -> Result<()> {
let data = TestData::start(false).await;
// fetch alpha instance on beta
data.beta
.resolve_instance(Url::parse(&format!("http://{}", &data.alpha.hostname))?)
.await?;
let beta_instances = data.beta.list_instances().await?;
.await
.unwrap();
let beta_instances = data.beta.list_instances().await.unwrap();
assert_eq!(1, beta_instances.len());
// fetch beta instance on gamma
data.gamma
.resolve_instance(Url::parse(&format!("http://{}", &data.beta.hostname))?)
.await?;
.await
.unwrap();
// wait until instance collection is fetched
let gamma_instances = RetryFuture::new(
|| async {
let res = data.gamma.list_instances().await;
match res {
Err(_) => Err(RetryPolicy::<String>::Retry(None)),
Ok(i) if i.len() < 2 => Err(RetryPolicy::Retry(None)),
Ok(i) => Ok(i),
None => Err(RetryPolicy::<String>::Retry(None)),
Some(i) if i.len() < 2 => Err(RetryPolicy::Retry(None)),
Some(i) => Ok(i),
}
},
LinearRetryStrategy::new(),
@ -736,7 +766,7 @@ async fn test_synchronize_instances() -> MyResult<()> {
}
#[tokio::test]
async fn test_article_approval_required() -> MyResult<()> {
async fn test_article_approval_required() -> Result<()> {
let data = TestData::start(true).await;
// create article
@ -745,10 +775,10 @@ async fn test_article_approval_required() -> MyResult<()> {
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
let create_res = data.alpha.create_article(&create_form).await.unwrap();
assert!(!create_res.article.approved);
let list_all = data.alpha.list_articles(Default::default()).await?;
let list_all = data.alpha.list_articles(Default::default()).await.unwrap();
assert_eq!(1, list_all.len());
assert!(list_all.iter().all(|a| a.id != create_res.article.id));
@ -757,26 +787,26 @@ async fn test_article_approval_required() -> MyResult<()> {
username: "ibis".to_string(),
password: "ibis".to_string(),
};
data.alpha.login(form).await?;
data.alpha.login(form).await.unwrap();
assert_eq!(1, data.alpha.notifications_count().await?);
let notifications = data.alpha.notifications_list().await?;
assert_eq!(1, data.alpha.notifications_count().await.unwrap());
let notifications = data.alpha.notifications_list().await.unwrap();
assert_eq!(1, notifications.len());
let Notification::ArticleApprovalRequired(notif) = &notifications[0] else {
panic!()
};
assert_eq!(create_res.article.id, notif.id);
data.alpha.approve_article(notif.id, true).await?;
data.alpha.approve_article(notif.id, true).await.unwrap();
let form = GetArticleForm {
id: Some(create_res.article.id),
..Default::default()
};
let approved = data.alpha.get_article(form).await?;
let approved = data.alpha.get_article(form).await.unwrap();
assert_eq!(create_res.article.id, approved.article.id);
assert!(approved.article.approved);
let list_all = data.alpha.list_articles(Default::default()).await?;
let list_all = data.alpha.list_articles(Default::default()).await.unwrap();
assert_eq!(2, list_all.len());
assert!(list_all.iter().any(|a| a.id == create_res.article.id));