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]]
name = "activitypub_federation"
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 = [
"activitystreams-kinds",
"async-trait",

View File

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

View File

@ -33,16 +33,14 @@ impl CreateArticle {
let local_instance = data.local_instance();
let object = article.clone().into_json(data).await?;
let id = generate_activity_id(local_instance.ap_id.inner())?;
let create_or_update = CreateArticle {
let create = CreateArticle {
actor: local_instance.ap_id.clone(),
to: local_instance.follower_ids(),
object,
kind: Default::default(),
id,
};
local_instance
.send_to_followers(create_or_update, data)
.await?;
local_instance.send_to_followers(create, data).await?;
Ok(())
}
}
@ -64,7 +62,10 @@ impl ActivityHandler for CreateArticle {
}
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(())
}
}

View File

@ -36,7 +36,8 @@ impl UpdateArticle {
) -> MyResult<()> {
let local_instance = data.local_instance();
let id = generate_activity_id(local_instance.ap_id.inner())?;
let create_or_update = UpdateArticle {
if article.local {
let update = UpdateArticle {
actor: local_instance.ap_id.clone(),
to: local_instance.follower_ids(),
object: article.ap_id,
@ -44,9 +45,21 @@ impl UpdateArticle {
kind: Default::default(),
id,
};
local_instance.send_to_followers(update, data).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_to_followers(create_or_update, data)
.send(update, vec![article_instance.inbox], data)
.await?;
}
Ok(())
}
}
@ -68,6 +81,7 @@ impl ActivityHandler for UpdateArticle {
}
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
let article_local = {
let edit = DbEdit::from_json(self.result.clone(), data).await?;
let mut lock = data.articles.lock().unwrap();
let article = lock.get_mut(self.object.inner()).unwrap();
@ -75,6 +89,27 @@ impl ActivityHandler for UpdateArticle {
// TODO: probably better to apply patch inside DbEdit::from_json()
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(())
}
}

View File

@ -81,10 +81,7 @@ impl Collection for DbArticleCollection {
.map(|i| DbArticle::from_json(i, data)),
)
.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
Ok(DbArticleCollection(articles))
}

View File

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

View File

@ -13,7 +13,7 @@ use url::Url;
#[tokio::test]
#[serial]
async fn test_create_and_read_article() -> MyResult<()> {
async fn test_create_read_and_edit_article() -> MyResult<()> {
let data = TestData::start();
// error on nonexistent article
@ -31,7 +31,6 @@ async fn test_create_and_read_article() -> MyResult<()> {
// create article
let create_article = CreateArticleData {
title: get_article.title.to_string(),
text: "Lorem ipsum".to_string(),
};
let create_res: DbArticle = post(data.hostname_alpha, "article", &create_article).await?;
assert_eq!(create_article.title, create_res.title);
@ -45,9 +44,18 @@ async fn test_create_and_read_article() -> MyResult<()> {
)
.await?;
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);
// 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()
}
@ -82,10 +90,10 @@ async fn test_synchronize_articles() -> MyResult<()> {
// create article on alpha
let create_article = CreateArticleData {
title: "Manu_Chao".to_string(),
text: "Lorem ipsum".to_string(),
};
let create_res: DbArticle = post(data.hostname_alpha, "article", &create_article).await?;
assert_eq!(create_article.title, create_res.title);
assert_eq!(0, create_res.edits.len());
assert!(create_res.local);
// article is not yet on beta
@ -115,7 +123,8 @@ async fn test_synchronize_articles() -> MyResult<()> {
.await?;
assert_eq!(create_res.ap_id, get_res.ap_id);
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);
data.stop()
@ -123,7 +132,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
#[tokio::test]
#[serial]
async fn test_federate_article_changes() -> MyResult<()> {
async fn test_edit_local_article() -> MyResult<()> {
let data = TestData::start();
follow_instance(data.hostname_alpha, data.hostname_beta).await?;
@ -131,10 +140,10 @@ async fn test_federate_article_changes() -> MyResult<()> {
// create new article
let create_form = CreateArticleData {
title: "Manu_Chao".to_string(),
text: "Lorem ipsum".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
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()))
.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);
// 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()))
.await?;
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);
data.stop()