Add test case for federating edit of remote article

This commit is contained in:
Felix Ableitner 2023-11-22 15:59:22 +01:00
parent 4e458650b8
commit 61682100f2
7 changed files with 157 additions and 38 deletions

2
Cargo.lock generated
View File

@ -5,7 +5,7 @@ version = 3
[[package]] [[package]]
name = "activitypub_federation" name = "activitypub_federation"
version = "0.5.0-beta.5" version = "0.5.0-beta.5"
source = "git+https://github.com/LemmyNet/activitypub-federation-rust.git?branch=parse-impl#b80408d80619ac014a5cedf5079967c20058532d" source = "git+https://github.com/LemmyNet/activitypub-federation-rust.git?branch=parse-impl#2aa64ad1de7943840677f4b96a20a11d38e2be56"
dependencies = [ dependencies = [
"activitystreams-kinds", "activitystreams-kinds",
"async-trait", "async-trait",

View File

@ -29,10 +29,8 @@ pub fn api_routes() -> Router {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct CreateArticleData { pub struct CreateArticleData {
pub title: String, pub title: String,
pub text: String,
} }
// TODO: new article should be created with empty content
#[debug_handler] #[debug_handler]
async fn create_article( async fn create_article(
data: Data<DatabaseHandle>, data: Data<DatabaseHandle>,
@ -48,7 +46,7 @@ async fn create_article(
.into(); .into();
let article = DbArticle { let article = DbArticle {
title: create_article.title, title: create_article.title,
text: create_article.text, text: String::new(),
ap_id, ap_id,
edits: vec![], edits: vec![],
instance: local_instance_id, instance: local_instance_id,

View File

@ -33,16 +33,14 @@ impl CreateArticle {
let local_instance = data.local_instance(); let local_instance = data.local_instance();
let object = article.clone().into_json(data).await?; let object = article.clone().into_json(data).await?;
let id = generate_activity_id(local_instance.ap_id.inner())?; let id = generate_activity_id(local_instance.ap_id.inner())?;
let create_or_update = CreateArticle { let create = CreateArticle {
actor: local_instance.ap_id.clone(), actor: local_instance.ap_id.clone(),
to: local_instance.follower_ids(), to: local_instance.follower_ids(),
object, object,
kind: Default::default(), kind: Default::default(),
id, id,
}; };
local_instance local_instance.send_to_followers(create, data).await?;
.send_to_followers(create_or_update, data)
.await?;
Ok(()) Ok(())
} }
} }
@ -64,7 +62,10 @@ impl ActivityHandler for CreateArticle {
} }
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> { async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
DbArticle::from_json(self.object, data).await?; let article = DbArticle::from_json(self.object.clone(), data).await?;
if article.local {
data.local_instance().send_to_followers(self, data).await?;
}
Ok(()) Ok(())
} }
} }

View File

@ -36,17 +36,30 @@ impl UpdateArticle {
) -> 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())?;
let create_or_update = UpdateArticle { if article.local {
actor: local_instance.ap_id.clone(), let update = UpdateArticle {
to: local_instance.follower_ids(), actor: local_instance.ap_id.clone(),
object: article.ap_id, to: local_instance.follower_ids(),
result: edit.into_json(data).await?, object: article.ap_id,
kind: Default::default(), result: edit.into_json(data).await?,
id, kind: Default::default(),
}; id,
local_instance };
.send_to_followers(create_or_update, data) local_instance.send_to_followers(update, data).await?;
.await?; } else {
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: article.ap_id,
result: edit.into_json(data).await?,
kind: Default::default(),
id,
};
local_instance
.send(update, vec![article_instance.inbox], data)
.await?;
}
Ok(()) Ok(())
} }
} }
@ -68,13 +81,35 @@ impl ActivityHandler for UpdateArticle {
} }
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> { async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
let edit = DbEdit::from_json(self.result.clone(), data).await?; let article_local = {
let mut lock = data.articles.lock().unwrap(); let edit = DbEdit::from_json(self.result.clone(), data).await?;
let article = lock.get_mut(self.object.inner()).unwrap(); let mut lock = data.articles.lock().unwrap();
article.edits.push(edit); let article = lock.get_mut(self.object.inner()).unwrap();
// TODO: probably better to apply patch inside DbEdit::from_json() article.edits.push(edit);
let patch = Patch::from_str(&self.result.diff)?; // TODO: probably better to apply patch inside DbEdit::from_json()
article.text = apply(&article.text, &patch)?; let patch = Patch::from_str(&self.result.diff)?;
article.text = apply(&article.text, &patch)?;
article.local
};
if article_local {
// No need to wrap in announce, we can construct a new activity as all important info
// is in the object and result fields.
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: self.object,
result: self.result,
kind: Default::default(),
id,
};
data.local_instance()
.send_to_followers(update, data)
.await?;
}
Ok(()) Ok(())
} }
} }

View File

@ -81,10 +81,7 @@ impl Collection for DbArticleCollection {
.map(|i| DbArticle::from_json(i, data)), .map(|i| DbArticle::from_json(i, data)),
) )
.await?; .await?;
let mut lock = data.articles.lock().unwrap();
for a in &articles {
lock.insert(a.ap_id.inner().clone(), a.clone());
}
// TODO: return value propably not needed // TODO: return value propably not needed
Ok(DbArticleCollection(articles)) Ok(DbArticleCollection(articles))
} }

View File

@ -16,8 +16,10 @@ pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
pub struct TestData { pub struct TestData {
pub hostname_alpha: &'static str, pub hostname_alpha: &'static str,
pub hostname_beta: &'static str, pub hostname_beta: &'static str,
pub hostname_gamma: &'static str,
handle_alpha: JoinHandle<()>, handle_alpha: JoinHandle<()>,
handle_beta: JoinHandle<()>, handle_beta: JoinHandle<()>,
handle_gamma: JoinHandle<()>,
} }
impl TestData { impl TestData {
@ -33,23 +35,30 @@ impl TestData {
let hostname_alpha = "localhost:8131"; let hostname_alpha = "localhost:8131";
let hostname_beta = "localhost:8132"; let hostname_beta = "localhost:8132";
let hostname_gamma = "localhost:8133";
let handle_alpha = tokio::task::spawn(async { let handle_alpha = tokio::task::spawn(async {
start(hostname_alpha).await.unwrap(); start(hostname_alpha).await.unwrap();
}); });
let handle_beta = tokio::task::spawn(async { let handle_beta = tokio::task::spawn(async {
start(hostname_beta).await.unwrap(); start(hostname_beta).await.unwrap();
}); });
let handle_gamma = tokio::task::spawn(async {
start(hostname_gamma).await.unwrap();
});
Self { Self {
hostname_alpha, hostname_alpha,
hostname_beta, hostname_beta,
hostname_gamma,
handle_alpha, handle_alpha,
handle_beta, handle_beta,
handle_gamma,
} }
} }
pub fn stop(self) -> MyResult<()> { pub fn stop(self) -> MyResult<()> {
self.handle_alpha.abort(); self.handle_alpha.abort();
self.handle_beta.abort(); self.handle_beta.abort();
self.handle_gamma.abort();
Ok(()) Ok(())
} }
} }

View File

@ -13,7 +13,7 @@ use url::Url;
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_create_and_read_article() -> MyResult<()> { async fn test_create_read_and_edit_article() -> MyResult<()> {
let data = TestData::start(); let data = TestData::start();
// error on nonexistent article // error on nonexistent article
@ -31,7 +31,6 @@ async fn test_create_and_read_article() -> MyResult<()> {
// create article // create article
let create_article = CreateArticleData { let create_article = CreateArticleData {
title: get_article.title.to_string(), title: get_article.title.to_string(),
text: "Lorem ipsum".to_string(),
}; };
let create_res: DbArticle = post(data.hostname_alpha, "article", &create_article).await?; let create_res: DbArticle = post(data.hostname_alpha, "article", &create_article).await?;
assert_eq!(create_article.title, create_res.title); assert_eq!(create_article.title, create_res.title);
@ -45,9 +44,18 @@ async fn test_create_and_read_article() -> MyResult<()> {
) )
.await?; .await?;
assert_eq!(create_article.title, get_res.title); assert_eq!(create_article.title, get_res.title);
assert_eq!(create_article.text, get_res.text); assert!(get_res.text.is_empty());
assert!(get_res.local); assert!(get_res.local);
// edit article
let edit_form = EditArticleData {
ap_id: create_res.ap_id.clone(),
new_text: "Lorem Ipsum 2".to_string(),
};
let edit_res: DbArticle = patch(data.hostname_alpha, "article", &edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.text);
assert_eq!(1, edit_res.edits.len());
data.stop() data.stop()
} }
@ -82,10 +90,10 @@ async fn test_synchronize_articles() -> MyResult<()> {
// create article on alpha // create article on alpha
let create_article = CreateArticleData { let create_article = CreateArticleData {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: "Lorem ipsum".to_string(),
}; };
let create_res: DbArticle = post(data.hostname_alpha, "article", &create_article).await?; let create_res: DbArticle = post(data.hostname_alpha, "article", &create_article).await?;
assert_eq!(create_article.title, create_res.title); assert_eq!(create_article.title, create_res.title);
assert_eq!(0, create_res.edits.len());
assert!(create_res.local); assert!(create_res.local);
// article is not yet on beta // article is not yet on beta
@ -115,7 +123,8 @@ async fn test_synchronize_articles() -> MyResult<()> {
.await?; .await?;
assert_eq!(create_res.ap_id, get_res.ap_id); assert_eq!(create_res.ap_id, get_res.ap_id);
assert_eq!(create_article.title, get_res.title); assert_eq!(create_article.title, get_res.title);
assert_eq!(create_article.text, get_res.text); assert_eq!(0, get_res.edits.len());
assert!(get_res.text.is_empty());
assert!(!get_res.local); assert!(!get_res.local);
data.stop() data.stop()
@ -123,7 +132,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_federate_article_changes() -> MyResult<()> { async fn test_edit_local_article() -> MyResult<()> {
let data = TestData::start(); let data = TestData::start();
follow_instance(data.hostname_alpha, data.hostname_beta).await?; follow_instance(data.hostname_alpha, data.hostname_beta).await?;
@ -131,10 +140,10 @@ async fn test_federate_article_changes() -> MyResult<()> {
// create new article // create new article
let create_form = CreateArticleData { let create_form = CreateArticleData {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: "Lorem ipsum".to_string(),
}; };
let create_res: DbArticle = post(data.hostname_beta, "article", &create_form).await?; let create_res: DbArticle = post(data.hostname_beta, "article", &create_form).await?;
assert_eq!(create_res.title, create_form.title); assert_eq!(create_res.title, create_form.title);
assert!(create_res.local);
// article should be federated to alpha // article should be federated to alpha
let get_article = GetArticleData { let get_article = GetArticleData {
@ -144,6 +153,8 @@ async fn test_federate_article_changes() -> MyResult<()> {
get_query::<DbArticle, _>(data.hostname_alpha, "article", Some(get_article.clone())) get_query::<DbArticle, _>(data.hostname_alpha, "article", Some(get_article.clone()))
.await?; .await?;
assert_eq!(create_res.title, get_res.title); assert_eq!(create_res.title, get_res.title);
assert_eq!(0, get_res.edits.len());
assert!(!get_res.local);
assert_eq!(create_res.text, get_res.text); assert_eq!(create_res.text, get_res.text);
// edit the article // edit the article
@ -167,6 +178,74 @@ async fn test_federate_article_changes() -> MyResult<()> {
get_query::<DbArticle, _>(data.hostname_alpha, "article", Some(get_article.clone())) get_query::<DbArticle, _>(data.hostname_alpha, "article", Some(get_article.clone()))
.await?; .await?;
assert_eq!(edit_res.title, get_res.title); assert_eq!(edit_res.title, get_res.title);
assert_eq!(edit_res.edits.len(), 1);
assert_eq!(edit_res.text, get_res.text);
data.stop()
}
#[tokio::test]
#[serial]
async fn test_edit_remote_article() -> MyResult<()> {
let data = TestData::start();
follow_instance(data.hostname_alpha, data.hostname_beta).await?;
follow_instance(data.hostname_gamma, data.hostname_beta).await?;
// create new article
let create_form = CreateArticleData {
title: "Manu_Chao".to_string(),
};
let create_res: DbArticle = post(data.hostname_beta, "article", &create_form).await?;
assert_eq!(create_res.title, create_form.title);
assert!(create_res.local);
// article should be federated to alpha and gamma
let get_article = GetArticleData {
title: create_res.title.clone(),
};
let get_res =
get_query::<DbArticle, _>(data.hostname_alpha, "article", Some(get_article.clone()))
.await?;
assert_eq!(create_res.title, get_res.title);
assert_eq!(0, get_res.edits.len());
assert!(!get_res.local);
assert_eq!(create_res.text, get_res.text);
let get_res =
get_query::<DbArticle, _>(data.hostname_gamma, "article", Some(get_article.clone()))
.await?;
assert_eq!(create_res.title, get_res.title);
assert_eq!(create_res.text, get_res.text);
let edit_form = EditArticleData {
ap_id: create_res.ap_id,
new_text: "Lorem Ipsum 2".to_string(),
};
let edit_res: DbArticle = patch(data.hostname_alpha, "article", &edit_form).await?;
assert_eq!(edit_res.text, edit_form.new_text);
assert_eq!(edit_res.edits.len(), 1);
assert!(!edit_res.local);
assert!(edit_res.edits[0]
.id
.to_string()
.starts_with(&edit_res.ap_id.to_string()));
// edit should be federated to beta and gamma
let get_article = GetArticleData {
title: edit_res.title.clone(),
};
let get_res =
get_query::<DbArticle, _>(data.hostname_beta, "article", Some(get_article.clone())).await?;
assert_eq!(edit_res.title, get_res.title);
assert_eq!(edit_res.edits.len(), 1);
assert_eq!(edit_res.text, get_res.text);
let get_res =
get_query::<DbArticle, _>(data.hostname_gamma, "article", Some(get_article.clone()))
.await?;
assert_eq!(edit_res.title, get_res.title);
assert_eq!(edit_res.edits.len(), 1);
assert_eq!(edit_res.text, get_res.text); assert_eq!(edit_res.text, get_res.text);
data.stop() data.stop()