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:
parent
ddad6e58a8
commit
fc3d625a98
18 changed files with 313 additions and 261 deletions
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(®ister_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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
pub mod backend;
|
||||
pub mod common;
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
pub mod frontend;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
268
tests/test.rs
268
tests/test.rs
|
@ -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) = ¬ifications[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) = ¬ifications[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) = ¬ifications[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));
|
||||
|
||||
|
|
Loading…
Reference in a new issue