partial test case for edit conflict

This commit is contained in:
Felix Ableitner 2023-11-24 15:31:31 +01:00
parent abb5ee0ce4
commit 9a5a195bfd
6 changed files with 97 additions and 73 deletions

View File

@ -1,10 +1,10 @@
use crate::database::DatabaseHandle; use crate::database::DatabaseHandle;
use crate::error::{MyResult}; use crate::error::MyResult;
use crate::federation::activities::create_article::CreateArticle; use crate::federation::activities::create_article::CreateArticle;
use crate::federation::activities::update_article::UpdateArticle; use crate::federation::activities::update_article::UpdateArticle;
use crate::federation::objects::article::{DbArticle}; use crate::federation::objects::article::DbArticle;
use crate::federation::objects::edit::DbEdit; use crate::federation::objects::edit::DbEdit;
use crate::federation::objects::instance::{DbInstance}; use crate::federation::objects::instance::DbInstance;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
@ -75,24 +75,34 @@ pub struct EditArticleData {
async fn edit_article( async fn edit_article(
data: Data<DatabaseHandle>, data: Data<DatabaseHandle>,
Form(edit_article): Form<EditArticleData>, Form(edit_article): Form<EditArticleData>,
) -> MyResult<Json<DbArticle>> { ) -> MyResult<()> {
let original_article = { let original_article = {
let mut lock = data.articles.lock().unwrap(); let mut lock = data.articles.lock().unwrap();
let article = lock.get_mut(edit_article.ap_id.inner()).unwrap(); let article = lock.get_mut(edit_article.ap_id.inner()).unwrap();
article.clone() article.clone()
}; };
let edit = DbEdit::new(&original_article, &edit_article.new_text)?; let edit = DbEdit::new(&original_article, &edit_article.new_text)?;
let updated_article = { if original_article.local {
let mut lock = data.articles.lock().unwrap(); let updated_article = {
let article = lock.get_mut(edit_article.ap_id.inner()).unwrap(); let mut lock = data.articles.lock().unwrap();
article.text = edit_article.new_text; let article = lock.get_mut(edit_article.ap_id.inner()).unwrap();
article.edits.push(edit.clone()); article.text = edit_article.new_text;
article.clone() article.edits.push(edit.clone());
}; article.clone()
};
UpdateArticle::send_to_followers(updated_article.clone(), edit, &data).await?; UpdateArticle::send_to_followers(edit, updated_article.clone(), &data).await?;
} else {
UpdateArticle::send_to_origin(
edit,
// TODO: should be dereference(), but then article is refetched which breaks test_edit_conflict()
original_article.instance.dereference_local(&data).await?,
&data,
)
.await?;
}
Ok(Json(updated_article)) Ok(())
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]

View File

@ -29,34 +29,41 @@ pub struct UpdateArticle {
impl UpdateArticle { impl UpdateArticle {
pub async fn send_to_followers( pub async fn send_to_followers(
article: DbArticle,
edit: DbEdit, edit: DbEdit,
article: DbArticle,
data: &Data<DatabaseHandle>,
) -> MyResult<()> {
debug_assert!(article.local);
let local_instance = data.local_instance();
let id = generate_activity_id(local_instance.ap_id.inner())?;
let update = UpdateArticle {
actor: local_instance.ap_id.clone(),
to: local_instance.follower_ids(),
object: edit.into_json(data).await?,
kind: Default::default(),
id,
};
local_instance.send_to_followers(update, data).await?;
Ok(())
}
pub async fn send_to_origin(
edit: DbEdit,
article_instance: DbInstance,
data: &Data<DatabaseHandle>, data: &Data<DatabaseHandle>,
) -> MyResult<()> { ) -> MyResult<()> {
let local_instance = data.local_instance(); let local_instance = data.local_instance();
let id = generate_activity_id(local_instance.ap_id.inner())?; let id = generate_activity_id(local_instance.ap_id.inner())?;
if article.local { let update = UpdateArticle {
let update = UpdateArticle { actor: local_instance.ap_id.clone(),
actor: local_instance.ap_id.clone(), to: vec![article_instance.ap_id.into_inner()],
to: local_instance.follower_ids(), object: edit.into_json(data).await?,
object: edit.into_json(data).await?, kind: Default::default(),
kind: Default::default(), id,
id, };
}; local_instance
local_instance.send_to_followers(update, data).await?; .send(update, vec![article_instance.inbox], data)
} else { .await?;
let article_instance = article.instance.dereference(data).await?;
let update = UpdateArticle {
actor: local_instance.ap_id.clone(),
to: vec![article_instance.ap_id.into_inner()],
object: edit.into_json(data).await?,
kind: Default::default(),
id,
};
local_instance
.send(update, vec![article_instance.inbox], data)
.await?;
}
Ok(()) Ok(())
} }
} }

View File

@ -91,6 +91,8 @@ impl Object for DbEdit {
let article = lock.get_mut(edit.article_id.inner()).unwrap(); let article = lock.get_mut(edit.article_id.inner()).unwrap();
article.edits.push(edit.clone()); article.edits.push(edit.clone());
let patch = Patch::from_str(&edit.diff)?; let patch = Patch::from_str(&edit.diff)?;
// TODO: this will give wrong result if new article text is federated, and then also new
// edit is applied. probably need to keep track of versions
article.text = apply(&article.text, &patch)?; article.text = apply(&article.text, &patch)?;
Ok(edit) Ok(edit)

View File

@ -13,6 +13,7 @@ use activitypub_federation::{
use chrono::{DateTime, Local, Utc}; use chrono::{DateTime, Local, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
use tracing::warn;
use url::Url; use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -94,7 +95,10 @@ impl DbInstance {
let activity = WithContext::new_default(activity); let activity = WithContext::new_default(activity);
let sends = SendActivityTask::prepare(&activity, self, recipients, data).await?; let sends = SendActivityTask::prepare(&activity, self, recipients, data).await?;
for send in sends { for send in sends {
send.sign_and_send(data).await?; let send = send.sign_and_send(data).await;
if let Err(e) = send {
warn!("Failed to send activity {:?}: {e}", activity);
}
} }
Ok(()) Ok(())
} }

View File

@ -1,5 +1,6 @@
use fediwiki::api::{FollowInstance, ResolveObject}; use fediwiki::api::{EditArticleData, FollowInstance, GetArticleData, ResolveObject};
use fediwiki::error::MyResult; use fediwiki::error::MyResult;
use fediwiki::federation::objects::article::DbArticle;
use fediwiki::federation::objects::instance::DbInstance; use fediwiki::federation::objects::instance::DbInstance;
use fediwiki::start; use fediwiki::start;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -63,6 +64,24 @@ impl TestData {
} }
} }
pub async fn edit_article(
hostname: &str,
title: &str,
edit_form: &EditArticleData,
) -> MyResult<DbArticle> {
CLIENT
.patch(format!("http://{}/api/v1/article", hostname))
.form(edit_form)
.send()
.await?;
let get_article = GetArticleData {
title: title.to_string(),
};
let updated_article: DbArticle =
get_query(hostname, &"article".to_string(), Some(get_article)).await?;
Ok(updated_article)
}
pub async fn get<T>(hostname: &str, endpoint: &str) -> MyResult<T> pub async fn get<T>(hostname: &str, endpoint: &str) -> MyResult<T>
where where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
@ -96,19 +115,6 @@ where
.await?) .await?)
} }
pub async fn patch<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
where
R: for<'de> Deserialize<'de>,
{
Ok(CLIENT
.patch(format!("http://{}/api/v1/{}", hostname, endpoint))
.form(form)
.send()
.await?
.json()
.await?)
}
pub async fn follow_instance(follow_instance: &str, followed_instance: &str) -> MyResult<()> { pub async fn follow_instance(follow_instance: &str, followed_instance: &str) -> MyResult<()> {
// fetch beta instance on alpha // fetch beta instance on alpha
let resolve_form = ResolveObject { let resolve_form = ResolveObject {

View File

@ -2,7 +2,7 @@ extern crate fediwiki;
mod common; mod common;
use crate::common::{follow_instance, get_query, patch, post, TestData}; use crate::common::{edit_article, follow_instance, get_query, post, TestData};
use common::get; use common::get;
use fediwiki::api::{CreateArticleData, EditArticleData, GetArticleData, ResolveObject}; use fediwiki::api::{CreateArticleData, EditArticleData, GetArticleData, ResolveObject};
use fediwiki::error::MyResult; use fediwiki::error::MyResult;
@ -52,7 +52,7 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
ap_id: create_res.ap_id.clone(), ap_id: create_res.ap_id.clone(),
new_text: "Lorem Ipsum 2".to_string(), new_text: "Lorem Ipsum 2".to_string(),
}; };
let edit_res: DbArticle = patch(data.hostname_alpha, "article", &edit_form).await?; let edit_res = edit_article(data.hostname_alpha, &create_res.title, &edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.text); assert_eq!(edit_form.new_text, edit_res.text);
assert_eq!(1, edit_res.edits.len()); assert_eq!(1, edit_res.edits.len());
@ -163,7 +163,7 @@ async fn test_edit_local_article() -> MyResult<()> {
ap_id: create_res.ap_id, ap_id: create_res.ap_id,
new_text: "Lorem Ipsum 2".to_string(), new_text: "Lorem Ipsum 2".to_string(),
}; };
let edit_res: DbArticle = patch(data.hostname_beta, "article", &edit_form).await?; let edit_res = edit_article(data.hostname_beta, &create_res.title, &edit_form).await?;
assert_eq!(edit_res.text, edit_form.new_text); assert_eq!(edit_res.text, edit_form.new_text);
assert_eq!(edit_res.edits.len(), 1); assert_eq!(edit_res.edits.len(), 1);
assert!(edit_res.edits[0] assert!(edit_res.edits[0]
@ -222,7 +222,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
ap_id: create_res.ap_id, ap_id: create_res.ap_id,
new_text: "Lorem Ipsum 2".to_string(), new_text: "Lorem Ipsum 2".to_string(),
}; };
let edit_res: DbArticle = patch(data.hostname_alpha, "article", &edit_form).await?; let edit_res = edit_article(data.hostname_alpha, &create_res.title, &edit_form).await?;
assert_eq!(edit_res.text, edit_form.new_text); assert_eq!(edit_res.text, edit_form.new_text);
assert_eq!(edit_res.edits.len(), 1); assert_eq!(edit_res.edits.len(), 1);
assert!(!edit_res.local); assert!(!edit_res.local);
@ -266,12 +266,20 @@ async fn test_edit_conflict() -> MyResult<()> {
assert_eq!(create_res.title, create_form.title); assert_eq!(create_res.title, create_form.title);
assert!(create_res.local); assert!(create_res.local);
// fetch article to gamma
let resolve_object = ResolveObject {
id: create_res.ap_id.inner().clone(),
};
let resolve_res: DbArticle =
get_query(data.hostname_gamma, "resolve_article", Some(resolve_object)).await?;
assert_eq!(create_res.text, resolve_res.text);
// alpha edits article // alpha edits article
let edit_form = EditArticleData { let edit_form = EditArticleData {
ap_id: create_res.ap_id.clone(), ap_id: create_res.ap_id.clone(),
new_text: "Lorem Ipsum".to_string(), new_text: "Lorem Ipsum".to_string(),
}; };
let edit_res: DbArticle = patch(data.hostname_alpha, "article", &edit_form).await?; let edit_res = edit_article(data.hostname_alpha, &create_res.title, &edit_form).await?;
assert_eq!(edit_res.text, edit_form.new_text); assert_eq!(edit_res.text, edit_form.new_text);
assert_eq!(edit_res.edits.len(), 1); assert_eq!(edit_res.edits.len(), 1);
assert!(!edit_res.local); assert!(!edit_res.local);
@ -280,28 +288,15 @@ async fn test_edit_conflict() -> MyResult<()> {
.to_string() .to_string()
.starts_with(&edit_res.ap_id.to_string())); .starts_with(&edit_res.ap_id.to_string()));
// fetch article to gamma
let resolve_object = ResolveObject {
id: create_res.ap_id.inner().clone(),
};
get_query::<DbArticle, _>(data.hostname_gamma, "resolve_article", Some(resolve_object)).await?;
// gamma also edits, as its not the latest version there is a conflict // gamma also edits, as its not the latest version there is a conflict
// TODO: get this working
let edit_form = EditArticleData { let edit_form = EditArticleData {
ap_id: create_res.ap_id, ap_id: create_res.ap_id,
new_text: "Ipsum Lorem".to_string(), new_text: "aaaa".to_string(),
}; };
dbg!(&edit_form); let edit_res = edit_article(data.hostname_gamma, &create_res.title, &edit_form).await?;
let edit_res: DbArticle = dbg!(patch(data.hostname_gamma, "article", &edit_form).await)?; assert_eq!(create_res.text, edit_res.text);
dbg!(&edit_res); assert_eq!(0, edit_res.edits.len());
assert_eq!(edit_res.text, edit_form.new_text);
assert_eq!(edit_res.edits.len(), 1);
assert!(!edit_res.local); assert!(!edit_res.local);
assert!(edit_res.edits[0]
.id
.to_string()
.starts_with(&edit_res.ap_id.to_string()));
data.stop() data.stop()
} }