create article in ui

This commit is contained in:
Felix Ableitner 2024-01-30 16:06:02 +01:00
parent 98a9ca2439
commit a12895f9bf
13 changed files with 220 additions and 124 deletions

View File

@ -2,6 +2,7 @@
set -e set -e
# launch a couple of local instances to test federation # launch a couple of local instances to test federation
# sometimes ctrl+c doesnt work properly, so you have to kill trunk, cargo-watch and ibis manually
# TODO: somehow instances use wrong port resulting in cors errors # TODO: somehow instances use wrong port resulting in cors errors
(trap 'kill 0' SIGINT; (trap 'kill 0' SIGINT;
sh -c 'TRUNK_SERVE_PORT=8070 IBIS_BACKEND_PORT=8071 IBIS_DATABASE_URL="postgres://ibis:password@localhost:5432/ibis" ./scripts/watch.sh' & sh -c 'TRUNK_SERVE_PORT=8070 IBIS_BACKEND_PORT=8071 IBIS_DATABASE_URL="postgres://ibis:password@localhost:5432/ibis" ./scripts/watch.sh' &

View File

@ -26,14 +26,14 @@ use diffy::create_patch;
/// Create a new article with empty text, and federate it to followers. /// Create a new article with empty text, and federate it to followers.
#[debug_handler] #[debug_handler]
pub(in crate::backend::api) async fn create_article( pub(in crate::backend::api) async fn create_article(
Extension(_user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
data: Data<MyDataHandle>, data: Data<MyDataHandle>,
Form(create_article): Form<CreateArticleData>, Form(create_article): Form<CreateArticleData>,
) -> MyResult<Json<ArticleView>> { ) -> MyResult<Json<ArticleView>> {
let local_instance = DbInstance::read_local_instance(&data.db_connection)?; let local_instance = DbInstance::read_local_instance(&data.db_connection)?;
let ap_id = ObjectId::parse(&format!( let ap_id = ObjectId::parse(&format!(
"http://{}:{}/article/{}", "http://{}:{}/article/{}",
local_instance.ap_id.inner().domain().unwrap(), local_instance.ap_id.inner().host_str().unwrap(),
local_instance.ap_id.inner().port().unwrap(), local_instance.ap_id.inner().port().unwrap(),
create_article.title create_article.title
))?; ))?;
@ -46,9 +46,19 @@ pub(in crate::backend::api) async fn create_article(
}; };
let article = DbArticle::create(&form, &data.db_connection)?; let article = DbArticle::create(&form, &data.db_connection)?;
CreateArticle::send_to_followers(article.clone(), &data).await?; let edit_data = EditArticleData {
article_id: article.id,
new_text: create_article.text,
summary: create_article.summary,
previous_version_id: article.latest_edit_version(&data.db_connection)?,
resolve_conflict_id: None,
};
let _ = edit_article(Extension(user), data.reset_request_count(), Form(edit_data)).await?;
Ok(Json(DbArticle::read_view(article.id, &data.db_connection)?)) let article_view = DbArticle::read_view(article.id, &data.db_connection)?;
CreateArticle::send_to_followers(article_view.article.clone(), &data).await?;
Ok(Json(article_view))
} }
/// Edit an existing article (local or remote). /// Edit an existing article (local or remote).
@ -103,7 +113,7 @@ pub(in crate::backend::api) async fn edit_article(
let previous_version = DbEdit::read(&edit_form.previous_version_id, &data.db_connection)?; let previous_version = DbEdit::read(&edit_form.previous_version_id, &data.db_connection)?;
let form = DbConflictForm { let form = DbConflictForm {
id: EditVersion::new(&patch.to_string())?, id: EditVersion::new(&patch.to_string()),
diff: patch.to_string(), diff: patch.to_string(),
summary: edit_form.summary.clone(), summary: edit_form.summary.clone(),
creator_id: user.local_user.id, creator_id: user.local_user.id,

View File

@ -32,7 +32,7 @@ impl DbEditForm {
previous_version_id: EditVersion, previous_version_id: EditVersion,
) -> MyResult<Self> { ) -> MyResult<Self> {
let diff = create_patch(&original_article.text, updated_text); let diff = create_patch(&original_article.text, updated_text);
let version = EditVersion::new(&diff.to_string())?; let version = EditVersion::new(&diff.to_string());
let ap_id = Self::generate_ap_id(original_article, &version)?; let ap_id = Self::generate_ap_id(original_article, &version)?;
Ok(DbEditForm { Ok(DbEditForm {
hash: version, hash: version,

View File

@ -1,35 +1 @@
use crate::backend::error::MyResult;
use crate::common::EditVersion;
use sha2::{Digest, Sha256};
use uuid::Uuid;
impl EditVersion {
pub fn new(diff: &str) -> MyResult<Self> {
let mut sha256 = Sha256::new();
sha256.update(diff);
let hash_bytes = sha256.finalize();
let uuid = Uuid::from_slice(&hash_bytes.as_slice()[..16])?;
Ok(EditVersion(uuid))
}
pub fn hash(&self) -> String {
hex::encode(self.0.into_bytes())
}
}
impl Default for EditVersion {
fn default() -> Self {
EditVersion::new("").unwrap()
}
}
#[test]
fn test_edit_versions() -> MyResult<()> {
let default = EditVersion::default();
assert_eq!("e3b0c44298fc1c149afbf4c8996fb924", default.hash());
let version = EditVersion::new("test")?;
assert_eq!("9f86d081884c7d659a2feaa0c55ad015", version.hash());
Ok(())
}

View File

@ -75,7 +75,7 @@ impl ActivityHandler for RejectEdit {
let article = self.object.object.dereference(data).await?; let article = self.object.object.dereference(data).await?;
let creator = self.object.attributed_to.dereference(data).await?; let creator = self.object.attributed_to.dereference(data).await?;
let form = DbConflictForm { let form = DbConflictForm {
id: EditVersion::new(&self.object.content)?, id: EditVersion::new(&self.object.content),
diff: self.object.content, diff: self.object.content,
summary: self.object.summary, summary: self.object.summary,
creator_id: creator.id, creator_id: creator.id,

View File

@ -51,7 +51,7 @@ mod test {
Ok(DbEdit { Ok(DbEdit {
id: 0, id: 0,
creator_id: 0, creator_id: 0,
hash: EditVersion::new(&diff)?, hash: EditVersion::new(&diff),
ap_id: ObjectId::parse("http://example.com")?, ap_id: ObjectId::parse("http://example.com")?,
diff, diff,
summary: String::new(), summary: String::new(),
@ -79,7 +79,7 @@ mod test {
#[test] #[test]
fn test_generate_invalid_version() -> MyResult<()> { fn test_generate_invalid_version() -> MyResult<()> {
let edits = create_edits()?; let edits = create_edits()?;
let generated = generate_article_version(&edits, &EditVersion::new("invalid")?); let generated = generate_article_version(&edits, &EditVersion::new("invalid"));
assert!(generated.is_err()); assert!(generated.is_err());
Ok(()) Ok(())
} }

View File

@ -1,5 +1,6 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
@ -79,6 +80,26 @@ pub struct DbEdit {
#[cfg_attr(feature = "ssr", derive(diesel_derive_newtype::DieselNewType))] #[cfg_attr(feature = "ssr", derive(diesel_derive_newtype::DieselNewType))]
pub struct EditVersion(pub(crate) Uuid); pub struct EditVersion(pub(crate) Uuid);
impl EditVersion {
pub fn new(diff: &str) -> Self {
let mut sha256 = Sha256::new();
sha256.update(diff);
let hash_bytes = sha256.finalize();
let uuid = Uuid::from_slice(&hash_bytes.as_slice()[..16]).unwrap();
EditVersion(uuid)
}
pub fn hash(&self) -> String {
hex::encode(self.0.into_bytes())
}
}
impl Default for EditVersion {
fn default() -> Self {
EditVersion::new("")
}
}
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
pub struct RegisterUserData { pub struct RegisterUserData {
pub username: String, pub username: String,
@ -133,6 +154,8 @@ pub struct DbPerson {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct CreateArticleData { pub struct CreateArticleData {
pub title: String, pub title: String,
pub text: String,
pub summary: String,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
@ -213,3 +236,12 @@ pub struct InstanceView {
pub followers: Vec<DbPerson>, pub followers: Vec<DbPerson>,
pub following: Vec<DbInstance>, pub following: Vec<DbInstance>,
} }
#[test]
fn test_edit_versions() {
let default = EditVersion::default();
assert_eq!("e3b0c44298fc1c149afbf4c8996fb924", default.hash());
let version = EditVersion::new("test");
assert_eq!("9f86d081884c7d659a2feaa0c55ad015", version.hash());
}

View File

@ -56,26 +56,12 @@ impl ApiClient {
handle_json_res::<LocalUserView>(req).await handle_json_res::<LocalUserView>(req).await
} }
pub async fn create_article(&self, title: String, new_text: String) -> MyResult<ArticleView> { pub async fn create_article(&self, data: &CreateArticleData) -> MyResult<ArticleView> {
let create_form = CreateArticleData {
title: title.clone(),
};
let req = self let req = self
.client .client
.post(format!("http://{}/api/v1/article", &self.hostname)) .post(format!("http://{}/api/v1/article", &self.hostname))
.form(&create_form); .form(data);
let article: ArticleView = handle_json_res(req).await?; handle_json_res(req).await
// create initial edit to ensure that conflicts are generated (there are no conflicts on empty file)
// TODO: maybe take initial text directly in create article, no reason to have empty article
let edit_form = EditArticleData {
article_id: article.article.id,
new_text,
summary: "initial text".to_string(),
previous_version_id: article.latest_version,
resolve_conflict_id: None,
};
Ok(self.edit_article(&edit_form).await.unwrap())
} }
pub async fn edit_article_with_conflict( pub async fn edit_article_with_conflict(

View File

@ -1,6 +1,7 @@
use crate::common::LocalUserView; use crate::common::LocalUserView;
use crate::frontend::api::ApiClient; use crate::frontend::api::ApiClient;
use crate::frontend::components::nav::Nav; use crate::frontend::components::nav::Nav;
use crate::frontend::pages::article::create::CreateArticle;
use crate::frontend::pages::article::edit::EditArticle; use crate::frontend::pages::article::edit::EditArticle;
use crate::frontend::pages::article::history::ArticleHistory; use crate::frontend::pages::article::history::ArticleHistory;
use crate::frontend::pages::article::read::ReadArticle; use crate::frontend::pages::article::read::ReadArticle;
@ -74,6 +75,7 @@ pub fn App() -> impl IntoView {
<Route path="/article/:title/edit" view=EditArticle/> <Route path="/article/:title/edit" view=EditArticle/>
<Route path="/article/:title/history" view=ArticleHistory/> <Route path="/article/:title/history" view=ArticleHistory/>
<Route path="/article/:title/diff/:hash" view=EditDiff/> <Route path="/article/:title/diff/:hash" view=EditDiff/>
<Route path="/article/create" view=CreateArticle/>
<Route path={Page::Login.path()} view=Login/> <Route path={Page::Login.path()} view=Login/>
<Route path={Page::Register.path()} view=Register/> <Route path={Page::Register.path()} view=Register/>
<Route path="/search" view=Search/> <Route path="/search" view=Search/>

View File

@ -18,6 +18,12 @@ pub fn Nav() -> impl IntoView {
<li> <li>
<A href="/">"Main Page"</A> <A href="/">"Main Page"</A>
</li> </li>
<Show
when=move || global_state.with(|state| state.my_profile.is_some())>
<li>
<A href="/article/create">"Create Article"</A>
</li>
</Show>
<li> <li>
<form on:submit=move |ev| { <form on:submit=move |ev| {
ev.prevent_default(); ev.prevent_default();

View File

@ -0,0 +1,86 @@
use crate::common::CreateArticleData;
use crate::frontend::app::GlobalState;
use leptos::*;
use leptos_router::Redirect;
#[component]
pub fn CreateArticle() -> impl IntoView {
let (title, set_title) = create_signal(String::new());
let (text, set_text) = create_signal(String::new());
let (summary, set_summary) = create_signal(String::new());
let (create_response, set_create_response) = create_signal(None::<()>);
let (create_error, set_create_error) = create_signal(None::<String>);
let (wait_for_response, set_wait_for_response) = create_signal(false);
let button_is_disabled =
Signal::derive(move || wait_for_response.get() || summary.get().is_empty());
let submit_action = create_action(move |(title, text, summary): &(String, String, String)| {
let title = title.clone();
let text = text.clone();
let summary = summary.clone();
async move {
let form = CreateArticleData {
title,
text,
summary,
};
set_wait_for_response.update(|w| *w = true);
let res = GlobalState::api_client().create_article(&form).await;
set_wait_for_response.update(|w| *w = false);
match res {
Ok(_res) => {
set_create_response.update(|v| *v = Some(()));
set_create_error.update(|e| *e = None);
}
Err(err) => {
let msg = err.0.to_string();
log::warn!("Unable to create: {msg}");
set_create_error.update(|e| *e = Some(msg));
}
}
}
});
view! {
<Show
when=move || create_response.get().is_some()
fallback=move || {
view! {
<div class="item-view">
<input
type="text"
required
placeholder="Title"
prop:disabled=move || wait_for_response.get()
on:keyup=move |ev| {
let val = event_target_value(&ev);
set_title.update(|v| *v = val);
}
/>
<textarea placeholder="Article text..." on:keyup=move |ev| {
let val = event_target_value(&ev);
set_text.update(|p| *p = val);
} >
</textarea>
</div>
{move || {
create_error
.get()
.map(|err| {
view! { <p style="color:red;">{err}</p> }
})
}}
<input type="text" on:keyup=move |ev| {
let val = event_target_value(&ev);
set_summary.update(|p| *p = val);
}/>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| submit_action.dispatch((title.get(), text.get(), summary.get()))>
Submit
</button>
}
}>
<Redirect path={format!("/article/{}", title.get())} />
</Show>
}
}

View File

@ -1,3 +1,4 @@
pub mod create;
pub mod edit; pub mod edit;
pub mod history; pub mod history;
pub mod read; pub mod read;

View File

@ -3,8 +3,8 @@ extern crate ibis_lib;
mod common; mod common;
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT}; use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
use ibis_lib::common::SearchArticleData;
use ibis_lib::common::{ArticleView, EditArticleData, ForkArticleData, GetArticleData}; use ibis_lib::common::{ArticleView, EditArticleData, ForkArticleData, GetArticleData};
use ibis_lib::common::{CreateArticleData, SearchArticleData};
use ibis_lib::common::{LoginUserData, RegisterUserData}; use ibis_lib::common::{LoginUserData, RegisterUserData};
use ibis_lib::frontend::error::MyResult; use ibis_lib::frontend::error::MyResult;
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
@ -15,12 +15,13 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create article // create article
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.alpha text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(title, create_res.article.title); let create_res = data.alpha.create_article(&create_form).await?;
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local); assert!(create_res.article.local);
// now article can be read // now article can be read
@ -30,7 +31,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
id: None, id: None,
}; };
let get_res = data.alpha.get_article(get_article_data.clone()).await?; let get_res = data.alpha.get_article(get_article_data.clone()).await?;
assert_eq!(title, get_res.article.title); assert_eq!(create_form.title, get_res.article.title);
assert_eq!(TEST_ARTICLE_DEFAULT_TEXT, get_res.article.text); assert_eq!(TEST_ARTICLE_DEFAULT_TEXT, get_res.article.text);
assert!(get_res.article.local); assert!(get_res.article.local);
@ -52,7 +53,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
assert_eq!(edit_form.summary, edit_res.edits[1].summary); assert_eq!(edit_form.summary, edit_res.edits[1].summary);
let search_form = SearchArticleData { let search_form = SearchArticleData {
query: title.clone(), query: create_form.title.clone(),
}; };
let search_res = data.alpha.search(&search_form).await?; let search_res = data.alpha.search(&search_form).await?;
assert_eq!(1, search_res.len()); assert_eq!(1, search_res.len());
@ -66,18 +67,16 @@ async fn test_create_duplicate_article() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create article // create article
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.alpha text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(title, create_res.article.title); let create_res = data.alpha.create_article(&create_form).await?;
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local); assert!(create_res.article.local);
let create_res = data let create_res = data.alpha.create_article(&create_form).await;
.alpha
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string())
.await;
assert!(create_res.is_err()); assert!(create_res.is_err());
data.stop() data.stop()
@ -123,12 +122,13 @@ async fn test_synchronize_articles() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create article on alpha // create article on alpha
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.alpha text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(title, create_res.article.title); let create_res = data.alpha.create_article(&create_form).await?;
assert_eq!(create_form.title, create_res.article.title);
assert_eq!(1, create_res.edits.len()); assert_eq!(1, create_res.edits.len());
assert!(create_res.article.local); assert!(create_res.article.local);
@ -162,7 +162,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
get_article_data.instance_id = Some(instance.id); get_article_data.instance_id = Some(instance.id);
let get_res = data.beta.get_article(get_article_data).await?; let get_res = data.beta.get_article(get_article_data).await?;
assert_eq!(create_res.article.ap_id, get_res.article.ap_id); assert_eq!(create_res.article.ap_id, get_res.article.ap_id);
assert_eq!(title, get_res.article.title); assert_eq!(create_form.title, get_res.article.title);
assert_eq!(2, get_res.edits.len()); assert_eq!(2, get_res.edits.len());
assert_eq!(edit_form.new_text, get_res.article.text); assert_eq!(edit_form.new_text, get_res.article.text);
assert!(!get_res.article.local); assert!(!get_res.article.local);
@ -177,12 +177,13 @@ async fn test_edit_local_article() -> MyResult<()> {
let beta_instance = data.alpha.follow_instance(&data.beta.hostname).await?; let beta_instance = data.alpha.follow_instance(&data.beta.hostname).await?;
// create new article // create new article
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.beta text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(title, create_res.article.title); let create_res = data.beta.create_article(&create_form).await?;
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local); assert!(create_res.article.local);
// article should be federated to alpha // article should be federated to alpha
@ -230,12 +231,13 @@ async fn test_edit_remote_article() -> MyResult<()> {
let beta_id_on_gamma = data.gamma.follow_instance(&data.beta.hostname).await?; let beta_id_on_gamma = data.gamma.follow_instance(&data.beta.hostname).await?;
// create new article // create new article
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.beta text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(&title, &create_res.article.title); let create_res = data.beta.create_article(&create_form).await?;
assert_eq!(&create_form.title, &create_res.article.title);
assert!(create_res.article.local); assert!(create_res.article.local);
// article should be federated to alpha and gamma // article should be federated to alpha and gamma
@ -299,12 +301,13 @@ async fn test_local_edit_conflict() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create new article // create new article
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.alpha text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(title, create_res.article.title); let create_res = data.alpha.create_article(&create_form).await?;
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local); assert!(create_res.article.local);
// one user edits article // one user edits article
@ -361,12 +364,13 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
let beta_id_on_alpha = data.alpha.follow_instance(&data.beta.hostname).await?; let beta_id_on_alpha = data.alpha.follow_instance(&data.beta.hostname).await?;
// create new article // create new article
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.beta text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(title, create_res.article.title); let create_res = data.beta.create_article(&create_form).await?;
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local); assert!(create_res.article.local);
// fetch article to gamma // fetch article to gamma
@ -378,7 +382,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
// alpha edits article // alpha edits article
let get_article_data = GetArticleData { let get_article_data = GetArticleData {
title: Some(title.to_string()), title: Some(create_form.title.to_string()),
instance_id: Some(beta_id_on_alpha.id), instance_id: Some(beta_id_on_alpha.id),
id: None, id: None,
}; };
@ -441,12 +445,13 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create new article // create new article
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.alpha text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(title, create_res.article.title); let create_res = data.alpha.create_article(&create_form).await?;
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local); assert!(create_res.article.local);
// one user edits article // one user edits article
@ -483,12 +488,13 @@ async fn test_fork_article() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create article // create article
let title = "Manu_Chao".to_string(); let create_form = CreateArticleData {
let create_res = data title: "Manu_Chao".to_string(),
.alpha text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
.create_article(title.clone(), TEST_ARTICLE_DEFAULT_TEXT.to_string()) summary: "create article".to_string(),
.await?; };
assert_eq!(title, create_res.article.title); let create_res = data.alpha.create_article(&create_form).await?;
assert_eq!(create_form.title, create_res.article.title);
assert!(create_res.article.local); assert!(create_res.article.local);
// fetch on beta // fetch on beta
@ -520,7 +526,7 @@ async fn test_fork_article() -> MyResult<()> {
// now search returns two articles for this title (original and forked) // now search returns two articles for this title (original and forked)
let search_form = SearchArticleData { let search_form = SearchArticleData {
query: title.clone(), query: create_form.title.clone(),
}; };
let search_res = data.beta.search(&search_form).await?; let search_res = data.beta.search(&search_form).await?;
assert_eq!(2, search_res.len()); assert_eq!(2, search_res.len());