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

Use single api endpoint for notifications (conflicts and new articles)

This commit is contained in:
Felix Ableitner 2024-11-12 15:54:28 +01:00
parent 7531476066
commit 21c77c2f24
18 changed files with 231 additions and 158 deletions

View file

@ -42,7 +42,7 @@ use diffy::create_patch;
/// Create a new article with empty text, and federate it to followers.
#[debug_handler]
pub(in crate::backend::api) async fn create_article(
Extension(user): Extension<LocalUserView>,
Extension(mut user): Extension<LocalUserView>,
data: Data<IbisData>,
Form(create_article): Form<CreateArticleForm>,
) -> MyResult<Json<ArticleView>> {
@ -79,9 +79,13 @@ pub(in crate::backend::api) async fn create_article(
previous_version_id: article.latest_edit_version(&data)?,
resolve_conflict_id: None,
};
// workaround so the edit goes through
user.local_user.admin = true;
let _ = edit_article(Extension(user), data.reset_request_count(), Form(edit_data)).await?;
let article_view = DbArticle::read_view(article.id, &data)?;
// allow reading unapproved article here
let article_view = DbArticle::read_view(article.id, true, &data)?;
CreateArticle::send_to_followers(article_view.article.clone(), &data).await?;
Ok(Json(article_view))
@ -102,11 +106,12 @@ pub(in crate::backend::api) async fn edit_article(
data: Data<IbisData>,
Form(mut edit_form): Form<EditArticleForm>,
) -> MyResult<Json<Option<ApiConflict>>> {
let is_admin = check_is_admin(&user).is_ok();
// resolve conflict if any
if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
DbConflict::delete(resolve_conflict_id, &data)?;
}
let original_article = DbArticle::read_view(edit_form.article_id, &data)?;
let original_article = DbArticle::read_view(edit_form.article_id, is_admin, &data)?;
if edit_form.new_text == original_article.article.text {
return Err(anyhow!("Edit contains no changes").into());
}
@ -148,7 +153,7 @@ pub(in crate::backend::api) async fn edit_article(
previous_version_id: previous_version.hash,
};
let conflict = DbConflict::create(&form, &data)?;
Ok(Json(conflict.to_api_conflict(&data).await?))
Ok(Json(conflict.to_api_conflict(is_admin, &data).await?))
}
}
@ -156,19 +161,23 @@ pub(in crate::backend::api) async fn edit_article(
#[debug_handler]
pub(in crate::backend::api) async fn get_article(
Query(query): Query<GetArticleForm>,
Extension(user): Extension<LocalUserView>,
data: Data<IbisData>,
) -> MyResult<Json<ArticleView>> {
let is_admin = check_is_admin(&user).is_ok();
match (query.title, query.id) {
(Some(title), None) => Ok(Json(DbArticle::read_view_title(
&title,
query.domain,
is_admin,
&data,
)?)),
(None, Some(id)) => {
if query.domain.is_some() {
return Err(anyhow!("Cant combine id and instance_domain").into());
}
Ok(Json(DbArticle::read_view(id, &data)?))
let article = DbArticle::read_view(id, is_admin, &data)?;
Ok(Json(article))
}
_ => Err(anyhow!("Must pass exactly one of title, id").into()),
}
@ -190,12 +199,13 @@ pub(in crate::backend::api) async fn list_articles(
/// how an article should be edited.
#[debug_handler]
pub(in crate::backend::api) async fn fork_article(
Extension(_user): Extension<LocalUserView>,
Extension(user): Extension<LocalUserView>,
data: Data<IbisData>,
Form(fork_form): Form<ForkArticleForm>,
) -> MyResult<Json<ArticleView>> {
let is_admin = check_is_admin(&user).is_ok();
// TODO: lots of code duplicated from create_article(), can move it into helper
let original_article = DbArticle::read(fork_form.article_id, &data)?;
let original_article = DbArticle::read_view(fork_form.article_id, is_admin, &data)?;
let local_instance = DbInstance::read_local_instance(&data)?;
let ap_id = ObjectId::parse(&format!(
@ -206,18 +216,19 @@ pub(in crate::backend::api) async fn fork_article(
))?;
let form = DbArticleForm {
title: fork_form.new_title,
text: original_article.text.clone(),
text: original_article.article.text.clone(),
ap_id,
instance_id: local_instance.id,
local: true,
protected: false,
approved: data.config.article_approval,
approved: !data.config.article_approval,
};
let article = DbArticle::create(form, &data)?;
// copy edits to new article
// this could also be done in sql
let edits = DbEdit::read_for_article(&original_article, &data)?
let edits = original_article
.edits
.into_iter()
.map(|e| e.edit)
.collect::<Vec<_>>();
@ -238,7 +249,7 @@ pub(in crate::backend::api) async fn fork_article(
CreateArticle::send_to_followers(article.clone(), &data).await?;
Ok(Json(DbArticle::read_view(article.id, &data)?))
Ok(Json(DbArticle::read_view(article.id, is_admin, &data)?))
}
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
@ -288,17 +299,6 @@ pub(in crate::backend::api) async fn protect_article(
Ok(Json(article))
}
/// Get a list of all unresolved edit conflicts.
#[debug_handler]
pub async fn list_approval_required(
Extension(user): Extension<LocalUserView>,
data: Data<IbisData>,
) -> MyResult<Json<Vec<DbArticle>>> {
check_is_admin(&user)?;
let articles = DbArticle::list_approval_required(&data)?;
Ok(Json(articles))
}
/// Get a list of all unresolved edit conflicts.
#[debug_handler]
pub async fn approve_article(

View file

@ -22,28 +22,25 @@ use crate::{
AUTH_COOKIE,
},
},
database::{conflict::DbConflict, IbisData},
database::IbisData,
error::MyResult,
},
common::{ApiConflict, LocalUserView},
common::LocalUserView,
};
use activitypub_federation::config::Data;
use anyhow::anyhow;
use article::{approve_article, list_approval_required};
use article::approve_article;
use axum::{
body::Body,
http::{Request, StatusCode},
middleware::{self, Next},
response::Response,
routing::{get, post},
Extension,
Json,
Router,
};
use axum_extra::extract::CookieJar;
use axum_macros::debug_handler;
use futures::future::try_join_all;
use instance::list_remote_instances;
use user::{count_notifications, list_notifications};
pub mod article;
pub mod instance;
@ -59,18 +56,15 @@ pub fn api_routes() -> Router<()> {
.route("/article/fork", post(fork_article))
.route("/article/resolve", get(resolve_article))
.route("/article/protect", post(protect_article))
.route(
"/article/list/approval_required",
get(list_approval_required),
)
.route("/article/approve", post(approve_article))
.route("/edit_conflicts", get(edit_conflicts))
.route("/instance", get(get_instance))
.route("/instance/follow", post(follow_instance))
.route("/instance/resolve", get(resolve_instance))
.route("/instance/list", get(list_remote_instances))
.route("/search", get(search_article))
.route("/user", get(get_user))
.route("/user/notifications/list", get(list_notifications))
.route("/user/notifications/count", get(count_notifications))
.route("/account/register", post(register_user))
.route("/account/login", post(login_user))
.route("/account/my_profile", get(my_profile))
@ -99,21 +93,3 @@ fn check_is_admin(user: &LocalUserView) -> MyResult<()> {
}
Ok(())
}
/// Get a list of all unresolved edit conflicts.
#[debug_handler]
async fn edit_conflicts(
Extension(user): Extension<LocalUserView>,
data: Data<IbisData>,
) -> MyResult<Json<Vec<ApiConflict>>> {
let conflicts = DbConflict::list(&user.person, &data)?;
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
let data = data.reset_request_count();
async move { c.to_api_conflict(&data).await }
}))
.await?
.into_iter()
.flatten()
.collect();
Ok(Json(conflicts))
}

View file

@ -1,17 +1,27 @@
use super::check_is_admin;
use crate::{
backend::{
database::{read_jwt_secret, IbisData},
database::{conflict::DbConflict, read_jwt_secret, IbisData},
error::MyResult,
},
common::{DbPerson, GetUserForm, LocalUserView, LoginUserForm, RegisterUserForm},
common::{
DbArticle,
DbPerson,
GetUserForm,
LocalUserView,
LoginUserForm,
Notification,
RegisterUserForm,
},
};
use activitypub_federation::config::Data;
use anyhow::anyhow;
use axum::{extract::Query, Form, Json};
use axum::{extract::Query, Extension, Form, Json};
use axum_extra::extract::cookie::{Cookie, CookieJar, Expiration, SameSite};
use axum_macros::debug_handler;
use bcrypt::verify;
use chrono::Utc;
use futures::future::try_join_all;
use jsonwebtoken::{
decode,
encode,
@ -145,3 +155,49 @@ pub(in crate::backend::api) async fn get_user(
&data,
)?))
}
#[debug_handler]
pub(crate) async fn list_notifications(
Extension(user): Extension<LocalUserView>,
data: Data<IbisData>,
) -> MyResult<Json<Vec<Notification>>> {
let is_admin = check_is_admin(&user).is_ok();
let conflicts = DbConflict::list(&user.person, &data)?;
let conflicts: Vec<_> = try_join_all(conflicts.into_iter().map(|c| {
let data = data.reset_request_count();
async move { c.to_api_conflict(is_admin, &data).await }
}))
.await?;
let mut notifications: Vec<_> = conflicts
.into_iter()
.flatten()
.map(Notification::EditConflict)
.collect();
if check_is_admin(&user).is_ok() {
let articles = DbArticle::list_approval_required(&data)?;
notifications.extend(
articles
.into_iter()
.map(Notification::ArticleApprovalRequired),
)
}
Ok(Json(notifications))
}
#[debug_handler]
pub(crate) async fn count_notifications(
Extension(user): Extension<LocalUserView>,
data: Data<IbisData>,
) -> MyResult<Json<usize>> {
let mut count = 0;
let conflicts = DbConflict::list(&user.person, &data)?;
count += conflicts.len();
if check_is_admin(&user).is_ok() {
let articles = DbArticle::list_approval_required(&data)?;
count += articles.len();
}
Ok(Json(count))
}

View file

@ -87,14 +87,13 @@ impl DbArticle {
.get_result::<Self>(conn.deref_mut())?)
}
pub fn read(id: ArticleId, data: &IbisData) -> MyResult<Self> {
pub fn read_view(id: ArticleId, is_admin: bool, data: &IbisData) -> MyResult<ArticleView> {
let mut conn = data.db_pool.get()?;
Ok(article::table.find(id).get_result(conn.deref_mut())?)
}
pub fn read_view(id: ArticleId, data: &IbisData) -> MyResult<ArticleView> {
let mut conn = data.db_pool.get()?;
let article: DbArticle = { article::table.find(id).get_result(conn.deref_mut())? };
let mut query = article::table.find(id).into_boxed();
if !is_admin {
query = query.filter(article::dsl::approved.eq(true));
}
let article: DbArticle = query.get_result(conn.deref_mut())?;
let latest_version = article.latest_edit_version(data)?;
let edits = DbEdit::read_for_article(&article, data)?;
Ok(ArticleView {
@ -107,6 +106,7 @@ impl DbArticle {
pub fn read_view_title(
title: &str,
domain: Option<String>,
admin: bool,
data: &IbisData,
) -> MyResult<ArticleView> {
let mut conn = data.db_pool.get()?;
@ -115,11 +115,14 @@ impl DbArticle {
.inner_join(instance::table)
.filter(article::dsl::title.eq(title))
.into_boxed();
let query = if let Some(domain) = domain {
let mut query = if let Some(domain) = domain {
query.filter(instance::dsl::domain.eq(domain))
} else {
query.filter(article::dsl::local.eq(true))
};
if !admin {
query = query.filter(article::dsl::approved.eq(true));
}
query
.select(article::all_columns)
.get_result(conn.deref_mut())?
@ -140,14 +143,6 @@ impl DbArticle {
.get_result(conn.deref_mut())?)
}
pub fn read_local_title(title: &str, data: &IbisData) -> MyResult<Self> {
let mut conn = data.db_pool.get()?;
Ok(article::table
.filter(article::dsl::title.eq(title))
.filter(article::dsl::local.eq(true))
.get_result(conn.deref_mut())?)
}
/// Read all articles, ordered by most recently edited first.
///
/// TODO: Should get rid of only_local param and rely on instance_id

View file

@ -76,10 +76,14 @@ impl DbConflict {
Ok(delete(conflict::table.find(id)).get_result(conn.deref_mut())?)
}
pub async fn to_api_conflict(&self, data: &Data<IbisData>) -> MyResult<Option<ApiConflict>> {
let article = DbArticle::read(self.article_id, data)?;
pub async fn to_api_conflict(
&self,
is_admin: bool,
data: &Data<IbisData>,
) -> MyResult<Option<ApiConflict>> {
let article = DbArticle::read_view(self.article_id, is_admin, data)?;
// Make sure to get latest version from origin so that all conflicts can be resolved
let original_article = article.ap_id.dereference_forced(data).await?;
let original_article = article.article.ap_id.dereference_forced(data).await?;
// create common ancestor version
let edits = DbEdit::read_for_article(&original_article, data)?;

View file

@ -52,7 +52,7 @@ impl Object for DbEdit {
}
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
let article = DbArticle::read(self.article_id, data)?;
let article = DbArticle::read_view(self.article_id, false, data)?;
let creator = DbPerson::read(self.creator_id, data)?;
Ok(ApubEdit {
kind: PatchType::Patch,
@ -61,7 +61,7 @@ impl Object for DbEdit {
summary: self.summary,
version: self.hash,
previous_version: self.previous_version_id,
object: article.ap_id,
object: article.article.ap_id,
attributed_to: creator.ap_id,
published: self.created,
})

View file

@ -35,7 +35,7 @@ impl Collection for DbEditCollection {
owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self::Kind, Self::Error> {
let article = DbArticle::read_view(owner.id, data)?;
let article = DbArticle::read_view(owner.id, false, data)?;
let edits = future::try_join_all(
article

View file

@ -94,8 +94,8 @@ async fn http_get_article(
Path(title): Path<String>,
data: Data<IbisData>,
) -> MyResult<FederationJson<WithContext<ApubArticle>>> {
let article = DbArticle::read_local_title(&title, &data)?;
let json = article.into_json(&data).await?;
let article = DbArticle::read_view_title(&title, None, false, &data)?;
let json = article.article.into_json(&data).await?;
Ok(FederationJson(WithContext::new_default(json)))
}
@ -104,8 +104,8 @@ async fn http_get_article_edits(
Path(title): Path<String>,
data: Data<IbisData>,
) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> {
let article = DbArticle::read_local_title(&title, &data)?;
let json = DbEditCollection::read_local(&article, &data).await?;
let article = DbArticle::read_view_title(&title, None, false, &data)?;
let json = DbEditCollection::read_local(&article.article, &data).await?;
Ok(FederationJson(WithContext::new_default(json)))
}

View file

@ -250,6 +250,12 @@ pub struct ApiConflict {
pub previous_version_id: EditVersion,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Notification {
EditConflict(ApiConflict),
ArticleApprovalRequired(DbArticle),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))]

View file

@ -19,6 +19,7 @@ use crate::{
ListArticlesForm,
LocalUserView,
LoginUserForm,
Notification,
ProtectArticleForm,
RegisterUserForm,
ResolveObject,
@ -132,10 +133,17 @@ impl ApiClient {
.await
}
pub async fn list_articles_approval_required(&self) -> MyResult<Vec<DbArticle>> {
pub async fn notifications_list(&self) -> MyResult<Vec<Notification>> {
let req = self
.client
.get(self.request_endpoint("/api/v1/article/list/approval_required"));
.get(self.request_endpoint("/api/v1/user/notifications/list"));
handle_json_res(req).await
}
pub async fn notifications_count(&self) -> MyResult<usize> {
let req = self
.client
.get(self.request_endpoint("/api/v1/user/notifications/count"));
handle_json_res(req).await
}
@ -230,13 +238,6 @@ impl ApiClient {
handle_json_res(req).await
}
pub async fn get_conflicts(&self) -> MyResult<Vec<ApiConflict>> {
let req = self
.client
.get(self.request_endpoint("/api/v1/edit_conflicts"));
Ok(handle_json_res(req).await.unwrap())
}
pub async fn resolve_article(&self, id: Url) -> MyResult<ArticleView> {
let resolve_object = ResolveObject { id };
self.get_query("/api/v1/article/resolve", Some(resolve_object))

View file

@ -13,10 +13,10 @@ use crate::{
list::ListArticles,
read::ReadArticle,
},
conflicts::Conflicts,
diff::EditDiff,
instance::{details::InstanceDetails, list::ListInstances},
login::Login,
notifications::Notifications,
register::Register,
search::Search,
user_profile::UserProfile,
@ -125,7 +125,7 @@ pub fn App() -> impl IntoView {
<Route path="/login" view=Login />
<Route path="/register" view=Register />
<Route path="/search" view=Search />
<Route path="/conflicts" view=Conflicts />
<Route path="/notifications" view=Notifications />
</Routes>
</main>
</Router>

View file

@ -19,6 +19,15 @@ pub fn Nav() -> impl IntoView {
.unwrap_or_default()
},
);
let notification_count = create_resource(
|| (),
move |_| async move {
GlobalState::api_client()
.notifications_count()
.await
.unwrap_or_default()
},
);
let (search_query, set_search_query) = create_signal(String::new());
let mut dark_mode = expect_context::<DarkMode>();
@ -57,7 +66,7 @@ pub fn Nav() -> impl IntoView {
<A href="/article/create">"Create Article"</A>
</li>
<li>
<A href="/conflicts">"Edit Conflicts"</A>
<A href="/notifications">"Notifications "<span class="indicator-item indicator-end badge badge-neutral">{notification_count}</span></A>
</li>
</Show>
<li>

View file

@ -1,5 +1,5 @@
use crate::{
common::{newtypes::ConflictId, ApiConflict, ArticleView, EditArticleForm},
common::{newtypes::ConflictId, ApiConflict, ArticleView, EditArticleForm, Notification},
frontend::{
app::GlobalState,
components::{
@ -35,10 +35,14 @@ pub fn EditArticle() -> impl IntoView {
let conflict_id = ConflictId(conflict_id.parse().unwrap());
async move {
let conflict = GlobalState::api_client()
.get_conflicts()
.notifications_list()
.await
.unwrap()
.into_iter()
.filter_map(|n| match n {
Notification::EditConflict(c) => Some(c),
_ => None,
})
.find(|c| c.id == conflict_id)
.unwrap();
set_edit_response.set(EditResponse::Conflict(conflict));

View file

@ -1,41 +0,0 @@
use crate::frontend::{app::GlobalState, article_link, article_title};
use leptos::*;
#[component]
pub fn Conflicts() -> impl IntoView {
let conflicts = create_resource(
move || {},
|_| async move { GlobalState::api_client().get_conflicts().await.unwrap() },
);
view! {
<h1>Your unresolved edit conflicts</h1>
<Suspense fallback=|| view! { "Loading..." }>
<ul>
{move || {
conflicts
.get()
.map(|c| {
c.into_iter()
.map(|c| {
let link = format!(
"{}/edit/{}",
article_link(&c.article),
c.id.0,
);
view! {
<li>
<a href=link>
{article_title(&c.article)} " - " {c.summary}
</a>
</li>
}
})
.collect::<Vec<_>>()
})
}}
</ul>
</Suspense>
}
}

View file

@ -6,10 +6,10 @@ use leptos::{create_resource, Resource, SignalGet};
use leptos_router::use_params_map;
pub(crate) mod article;
pub(crate) mod conflicts;
pub(crate) mod diff;
pub(crate) mod instance;
pub(crate) mod login;
pub(crate) mod notifications;
pub(crate) mod register;
pub(crate) mod search;
pub(crate) mod user_profile;

View file

@ -0,0 +1,55 @@
use crate::{
common::Notification,
frontend::{app::GlobalState, article_link, article_title},
};
use leptos::*;
#[component]
pub fn Notifications() -> impl IntoView {
let notifications = create_local_resource(
move || {},
|_| async move {
GlobalState::api_client()
.notifications_list()
.await
.unwrap()
},
);
view! {
<h1 class="text-4xl font-bold font-serif my-6 grow flex-auto">Notifications</h1>
<Suspense fallback=|| view! { "Loading..." }>
<ul>
{move || {
notifications
.get()
.map(|n| {
n.into_iter()
.map(|n| {
use Notification::*;
let (link, title) = match n {
EditConflict(c) => (format!(
"{}/edit/{}",
article_link(&c.article),
c.id.0)
, format!("Conflict: {} - {}", article_title(&c.article), c.summary)),
ArticleApprovalRequired(a) => (article_link(&a), format!("Approval required: {}", a.title)),
};
// TODO: need buttons to approve/reject new article, also makes sense to discard edit conflict
view! {
<li>
<a class="link text-lg" href=link>
{title}
</a>
</li>
}
})
.collect::<Vec<_>>()
})
}}
</ul>
</Suspense>
}
}

View file

@ -2,7 +2,7 @@
use ibis::{
backend::{
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation, IbisConfigSetup},
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
start,
},
common::RegisterUserForm,

View file

@ -14,6 +14,7 @@ use ibis::{
GetUserForm,
ListArticlesForm,
LoginUserForm,
Notification,
ProtectArticleForm,
RegisterUserForm,
SearchArticleForm,
@ -377,9 +378,12 @@ async fn test_local_edit_conflict() -> MyResult<()> {
.unwrap();
assert_eq!("<<<<<<< ours\nIpsum Lorem\n||||||| original\nsome\nexample\ntext\n=======\nLorem Ipsum\n>>>>>>> theirs\n", edit_res.three_way_merge);
let conflicts = data.alpha.get_conflicts().await?;
assert_eq!(1, conflicts.len());
assert_eq!(conflicts[0], edit_res);
let notifications = data.alpha.notifications_list().await?;
assert_eq!(1, notifications.len());
let Notification::EditConflict(conflict) = &notifications[0] else {
panic!()
};
assert_eq!(conflict, &edit_res);
let edit_form = EditArticleForm {
article_id: create_res.article.id,
@ -391,8 +395,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.article.text);
let conflicts = data.alpha.get_conflicts().await?;
assert_eq!(0, conflicts.len());
assert_eq!(0, data.alpha.notifications_count().await?);
data.stop()
}
@ -463,23 +466,28 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
assert_eq!(1, edit_res.edits.len());
assert!(!edit_res.article.local);
let conflicts = data.gamma.get_conflicts().await?;
assert_eq!(1, conflicts.len());
assert_eq!(1, data.gamma.notifications_count().await?);
let notifications = data.gamma.notifications_list().await?;
assert_eq!(1, notifications.len());
let Notification::EditConflict(conflict) = &notifications[0] else {
panic!()
};
// resolve the conflict
let edit_form = EditArticleForm {
article_id: resolve_res.article.id,
new_text: "aaaa\n".to_string(),
summary: "summary".to_string(),
previous_version_id: conflicts[0].previous_version_id.clone(),
resolve_conflict_id: Some(conflicts[0].id),
previous_version_id: conflict.previous_version_id.clone(),
resolve_conflict_id: Some(conflict.id),
};
let edit_res = data.gamma.edit_article(&edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(3, edit_res.edits.len());
let conflicts = data.gamma.get_conflicts().await?;
assert_eq!(0, conflicts.len());
assert_eq!(0, data.gamma.notifications_count().await?);
let notifications = data.gamma.notifications_list().await?;
assert_eq!(0, notifications.len());
data.stop()
}
@ -519,8 +527,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
resolve_conflict_id: None,
};
let edit_res = data.alpha.edit_article(&edit_form).await?;
let conflicts = data.alpha.get_conflicts().await?;
assert_eq!(0, conflicts.len());
assert_eq!(0, data.alpha.notifications_count().await?);
assert_eq!(3, edit_res.edits.len());
assert_eq!("my\nexample\narticle\n", edit_res.article.text);
@ -752,14 +759,15 @@ async fn test_article_approval_required() -> MyResult<()> {
};
data.alpha.login(form).await?;
let list_approval_required = data.alpha.list_articles_approval_required().await?;
assert_eq!(1, list_approval_required.len());
assert_eq!(create_res.article.id, list_approval_required[0].id);
assert_eq!(1, data.alpha.notifications_count().await?);
let notifications = data.alpha.notifications_list().await?;
assert_eq!(1, notifications.len());
let Notification::ArticleApprovalRequired(notif) = &notifications[0] else {
panic!()
};
assert_eq!(create_res.article.id, notif.id);
let approve = data
.alpha
.approve_article(list_approval_required[0].id)
.await?;
let approve = data.alpha.approve_article(notif.id).await?;
assert_eq!(create_res.article.id, approve.id);
assert!(approve.approved);