mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 12:41:10 +00:00
partial test case for edit conflict
This commit is contained in:
parent
abb5ee0ce4
commit
9a5a195bfd
6 changed files with 97 additions and 73 deletions
36
src/api.rs
36
src/api.rs
|
@ -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)]
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue