mirror of
https://github.com/Nutomic/ibis.git
synced 2024-12-23 20:11:24 +00:00
fix integration tests
This commit is contained in:
parent
8ca01dee07
commit
150415e8ad
16 changed files with 365 additions and 195 deletions
|
@ -1,3 +1,4 @@
|
|||
use crate::backend::api::ResolveObject;
|
||||
use crate::backend::database::article::DbArticleForm;
|
||||
use crate::backend::database::conflict::{ApiConflict, DbConflict, DbConflictForm};
|
||||
use crate::backend::database::edit::DbEditForm;
|
||||
|
@ -13,6 +14,7 @@ use crate::common::LocalUserView;
|
|||
use crate::common::{ArticleView, DbArticle, DbEdit};
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use anyhow::anyhow;
|
||||
use axum::extract::Query;
|
||||
use axum::Extension;
|
||||
use axum::Form;
|
||||
|
@ -126,10 +128,20 @@ pub(in crate::backend::api) async fn get_article(
|
|||
Query(query): Query<GetArticleData>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
Ok(Json(DbArticle::read_view_title(
|
||||
&query.title,
|
||||
&data.db_connection,
|
||||
)?))
|
||||
match (query.title, query.id) {
|
||||
(Some(title), None) => Ok(Json(DbArticle::read_view_title(
|
||||
&title,
|
||||
&query.instance_id,
|
||||
&data.db_connection,
|
||||
)?)),
|
||||
(None, Some(id)) => {
|
||||
if query.instance_id.is_some() {
|
||||
return Err(anyhow!("Cant combine id and instance_id").into());
|
||||
}
|
||||
Ok(Json(DbArticle::read_view(id, &data.db_connection)?))
|
||||
}
|
||||
_ => Err(anyhow!("Must pass exactly one of title, id").into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
@ -187,3 +199,20 @@ pub(in crate::backend::api) async fn fork_article(
|
|||
|
||||
Ok(Json(DbArticle::read_view(article.id, &data.db_connection)?))
|
||||
}
|
||||
|
||||
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
||||
/// article changes can only be received if we follow the instance, or if it is refetched manually.
|
||||
#[debug_handler]
|
||||
pub(super) async fn resolve_article(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
||||
let latest_version = edits.last().unwrap().hash.clone();
|
||||
Ok(Json(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
latest_version,
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use crate::backend::api::ResolveObject;
|
||||
use crate::backend::database::instance::{DbInstance, InstanceView};
|
||||
use crate::backend::database::MyDataHandle;
|
||||
use crate::backend::error::MyResult;
|
||||
use crate::backend::federation::activities::follow::Follow;
|
||||
use crate::common::LocalUserView;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use axum::extract::Query;
|
||||
use axum::Extension;
|
||||
use axum::{Form, Json};
|
||||
use axum_macros::debug_handler;
|
||||
|
@ -38,3 +41,16 @@ pub(in crate::backend::api) async fn follow_instance(
|
|||
Follow::send(user.person, instance, &data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to
|
||||
/// the local instance, and allows for interactions such as following.
|
||||
#[debug_handler]
|
||||
pub(super) async fn resolve_instance(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
// TODO: workaround because axum makes it hard to have multiple routes on /
|
||||
let id = format!("{}instance", query.id);
|
||||
let instance: DbInstance = ObjectId::parse(&id)?.dereference(&data).await?;
|
||||
Ok(Json(instance))
|
||||
}
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
use crate::backend::api::article::create_article;
|
||||
use crate::backend::api::article::{create_article, resolve_article};
|
||||
use crate::backend::api::article::{edit_article, fork_article, get_article};
|
||||
use crate::backend::api::instance::follow_instance;
|
||||
use crate::backend::api::instance::get_local_instance;
|
||||
use crate::backend::api::user::my_profile;
|
||||
use crate::backend::api::instance::{follow_instance, resolve_instance};
|
||||
use crate::backend::api::user::register_user;
|
||||
use crate::backend::api::user::validate;
|
||||
use crate::backend::api::user::{login_user, logout_user};
|
||||
use crate::backend::api::user::{my_profile, AUTH_COOKIE};
|
||||
use crate::backend::database::conflict::{ApiConflict, DbConflict};
|
||||
use crate::backend::database::instance::DbInstance;
|
||||
use crate::backend::database::MyDataHandle;
|
||||
use crate::backend::error::MyResult;
|
||||
use crate::common::DbEdit;
|
||||
use crate::common::DbArticle;
|
||||
use crate::common::LocalUserView;
|
||||
use crate::common::{ArticleView, DbArticle};
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use axum::extract::Query;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{
|
||||
extract::TypedHeader,
|
||||
headers::authorization::{Authorization, Bearer},
|
||||
http::Request,
|
||||
http::StatusCode,
|
||||
middleware::{self, Next},
|
||||
|
@ -27,6 +22,7 @@ use axum::{
|
|||
Extension,
|
||||
};
|
||||
use axum::{Json, Router};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use axum_macros::debug_handler;
|
||||
use futures::future::try_join_all;
|
||||
use log::warn;
|
||||
|
@ -44,11 +40,11 @@ pub fn api_routes() -> Router {
|
|||
get(get_article).post(create_article).patch(edit_article),
|
||||
)
|
||||
.route("/article/fork", post(fork_article))
|
||||
.route("/article/resolve", get(resolve_article))
|
||||
.route("/edit_conflicts", get(edit_conflicts))
|
||||
.route("/resolve_instance", get(resolve_instance))
|
||||
.route("/resolve_article", get(resolve_article))
|
||||
.route("/instance", get(get_local_instance))
|
||||
.route("/instance/follow", post(follow_instance))
|
||||
.route("/instance/resolve", get(resolve_instance))
|
||||
.route("/search", get(search_article))
|
||||
.route("/account/register", post(register_user))
|
||||
.route("/account/login", post(login_user))
|
||||
|
@ -59,12 +55,12 @@ pub fn api_routes() -> Router {
|
|||
|
||||
async fn auth<B>(
|
||||
data: Data<MyDataHandle>,
|
||||
auth: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
jar: CookieJar,
|
||||
mut request: Request<B>,
|
||||
next: Next<B>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
if let Some(auth) = auth {
|
||||
let user = validate(auth.token(), &data).await.map_err(|e| {
|
||||
if let Some(auth) = jar.get(AUTH_COOKIE) {
|
||||
let user = validate(auth.value(), &data).await.map_err(|e| {
|
||||
warn!("Failed to validate auth token: {e}");
|
||||
StatusCode::UNAUTHORIZED
|
||||
})?;
|
||||
|
@ -79,36 +75,6 @@ pub struct ResolveObject {
|
|||
pub id: Url,
|
||||
}
|
||||
|
||||
/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to
|
||||
/// the local instance, and allows for interactions such as following.
|
||||
#[debug_handler]
|
||||
async fn resolve_instance(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
// TODO: workaround because axum makes it hard to have multiple routes on /
|
||||
let id = format!("{}instance", query.id);
|
||||
let instance: DbInstance = ObjectId::parse(&id)?.dereference(&data).await?;
|
||||
Ok(Json(instance))
|
||||
}
|
||||
|
||||
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
||||
/// article changes can only be received if we follow the instance, or if it is refetched manually.
|
||||
#[debug_handler]
|
||||
async fn resolve_article(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
||||
let latest_version = edits.last().unwrap().hash.clone();
|
||||
Ok(Json(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
latest_version,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
async fn edit_conflicts(
|
||||
|
|
|
@ -60,7 +60,7 @@ pub(in crate::backend::api) async fn register_user(
|
|||
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
|
||||
let user = DbPerson::create_local(form.username, form.password, &data)?;
|
||||
let token = generate_login_token(&user.local_user, &data)?;
|
||||
let jar = jar.add(create_cookie(token));
|
||||
let jar = jar.add(create_cookie(token, &data));
|
||||
Ok((jar, Json(user)))
|
||||
}
|
||||
|
||||
|
@ -76,13 +76,18 @@ pub(in crate::backend::api) async fn login_user(
|
|||
return Err(anyhow!("Invalid login").into());
|
||||
}
|
||||
let token = generate_login_token(&user.local_user, &data)?;
|
||||
let jar = jar.add(create_cookie(token));
|
||||
let jar = jar.add(create_cookie(token, &data));
|
||||
Ok((jar, Json(user)))
|
||||
}
|
||||
|
||||
fn create_cookie(jwt: String) -> Cookie<'static> {
|
||||
fn create_cookie(jwt: String, data: &Data<MyDataHandle>) -> Cookie<'static> {
|
||||
let mut domain = data.domain().to_string();
|
||||
// remove port from domain
|
||||
if domain.contains(':') {
|
||||
domain = domain.split(':').collect::<Vec<_>>()[0].to_string();
|
||||
}
|
||||
Cookie::build(AUTH_COOKIE, jwt)
|
||||
.domain("localhost")
|
||||
.domain(domain)
|
||||
.same_site(SameSite::Strict)
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
|
|
|
@ -66,7 +66,7 @@ impl DbArticle {
|
|||
article::table.find(id).get_result(conn.deref_mut())?
|
||||
};
|
||||
let latest_version = article.latest_edit_version(conn)?;
|
||||
let edits: Vec<DbEdit> = DbEdit::read_for_article(&article, conn)?;
|
||||
let edits = DbEdit::read_for_article(&article, conn)?;
|
||||
Ok(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
|
@ -74,15 +74,25 @@ impl DbArticle {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn read_view_title(title: &str, conn: &Mutex<PgConnection>) -> MyResult<ArticleView> {
|
||||
pub fn read_view_title(
|
||||
title: &str,
|
||||
instance_id: &Option<i32>,
|
||||
conn: &Mutex<PgConnection>,
|
||||
) -> MyResult<ArticleView> {
|
||||
let article: DbArticle = {
|
||||
let mut conn = conn.lock().unwrap();
|
||||
article::table
|
||||
.filter(article::dsl::title.eq(title))
|
||||
.get_result(conn.deref_mut())?
|
||||
let query = article::table
|
||||
.into_boxed()
|
||||
.filter(article::dsl::title.eq(title));
|
||||
let query = if let Some(instance_id) = instance_id {
|
||||
query.filter(article::dsl::instance_id.eq(instance_id))
|
||||
} else {
|
||||
query.filter(article::dsl::local.eq(true))
|
||||
};
|
||||
query.get_result(conn.deref_mut())?
|
||||
};
|
||||
let latest_version = article.latest_edit_version(conn)?;
|
||||
let edits: Vec<DbEdit> = DbEdit::read_for_article(&article, conn)?;
|
||||
let edits = DbEdit::read_for_article(&article, conn)?;
|
||||
Ok(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
|
|
|
@ -71,9 +71,9 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
|
|||
|
||||
// Create the main page which is shown by default
|
||||
let form = DbArticleForm {
|
||||
title: "Main Page".to_string(),
|
||||
title: "Main_Page".to_string(),
|
||||
text: "Hello world!".to_string(),
|
||||
ap_id: ObjectId::parse("http://{hostname}/article/Main_Page")?,
|
||||
ap_id: ObjectId::parse(&format!("http://{hostname}/article/Main_Page"))?,
|
||||
instance_id: instance.id,
|
||||
local: true,
|
||||
};
|
||||
|
|
|
@ -8,9 +8,12 @@ use {
|
|||
diesel::{Identifiable, Queryable, Selectable},
|
||||
};
|
||||
|
||||
/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct GetArticleData {
|
||||
pub title: String,
|
||||
pub title: Option<String>,
|
||||
pub instance_id: Option<i32>,
|
||||
pub id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
|
|
|
@ -9,9 +9,52 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
|
||||
|
||||
pub async fn get_article(hostname: &str, title: String) -> MyResult<ArticleView> {
|
||||
let get_article = GetArticleData { title };
|
||||
get_query::<ArticleView, _>(hostname, "article", Some(get_article.clone())).await
|
||||
#[derive(Clone)]
|
||||
pub struct ApiClient {
|
||||
// TODO: make these private
|
||||
pub client: Client,
|
||||
pub hostname: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub fn new(client: Client, hostname: String) -> Self {
|
||||
Self { client, hostname }
|
||||
}
|
||||
|
||||
async fn get_query<T, R>(&self, endpoint: &str, query: Option<R>) -> MyResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize,
|
||||
{
|
||||
let mut req = self
|
||||
.client
|
||||
.get(format!("http://{}/api/v1/{}", &self.hostname, endpoint));
|
||||
if let Some(query) = query {
|
||||
req = req.query(&query);
|
||||
}
|
||||
handle_json_res::<T>(req).await
|
||||
}
|
||||
|
||||
pub async fn get_article(&self, data: GetArticleData) -> MyResult<ArticleView> {
|
||||
self.get_query::<ArticleView, _>("article", Some(data))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn register(&self, register_form: RegisterUserData) -> MyResult<LocalUserView> {
|
||||
let req = self
|
||||
.client
|
||||
.post(format!("http://{}/api/v1/account/register", self.hostname))
|
||||
.form(®ister_form);
|
||||
handle_json_res::<LocalUserView>(req).await
|
||||
}
|
||||
|
||||
pub async fn login(&self, login_form: LoginUserData) -> MyResult<LocalUserView> {
|
||||
let req = self
|
||||
.client
|
||||
.post(format!("http://{}/api/v1/account/login", self.hostname))
|
||||
.form(&login_form);
|
||||
handle_json_res::<LocalUserView>(req).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_query<T, R>(hostname: &str, endpoint: &str, query: Option<R>) -> MyResult<T>
|
||||
|
@ -40,20 +83,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn register(hostname: &str, register_form: RegisterUserData) -> MyResult<LocalUserView> {
|
||||
let req = CLIENT
|
||||
.post(format!("http://{}/api/v1/account/register", hostname))
|
||||
.form(®ister_form);
|
||||
handle_json_res::<LocalUserView>(req).await
|
||||
}
|
||||
|
||||
pub async fn login(hostname: &str, login_form: LoginUserData) -> MyResult<LocalUserView> {
|
||||
let req = CLIENT
|
||||
.post(format!("http://{}/api/v1/account/login", hostname))
|
||||
.form(&login_form);
|
||||
handle_json_res::<LocalUserView>(req).await
|
||||
}
|
||||
|
||||
pub async fn my_profile(hostname: &str) -> MyResult<LocalUserView> {
|
||||
let req = CLIENT.get(format!("http://{}/api/v1/account/my_profile", hostname));
|
||||
handle_json_res::<LocalUserView>(req).await
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::common::LocalUserView;
|
||||
use crate::frontend::api::my_profile;
|
||||
use crate::frontend::api::{my_profile, ApiClient};
|
||||
use crate::frontend::components::nav::Nav;
|
||||
use crate::frontend::pages::article::Article;
|
||||
use crate::frontend::pages::login::Login;
|
||||
|
@ -14,11 +14,14 @@ use leptos_meta::*;
|
|||
use leptos_router::Route;
|
||||
use leptos_router::Router;
|
||||
use leptos_router::Routes;
|
||||
use reqwest::Client;
|
||||
|
||||
// https://book.leptos.dev/15_global_state.html
|
||||
#[derive(Clone)]
|
||||
pub struct GlobalState {
|
||||
// TODO: remove
|
||||
backend_hostname: String,
|
||||
api_client: ApiClient,
|
||||
pub(crate) my_profile: Option<LocalUserView>,
|
||||
}
|
||||
|
||||
|
@ -29,6 +32,12 @@ impl GlobalState {
|
|||
.get_untracked()
|
||||
.backend_hostname
|
||||
}
|
||||
pub fn api_client() -> ApiClient {
|
||||
use_context::<RwSignal<GlobalState>>()
|
||||
.expect("global state is provided")
|
||||
.get_untracked()
|
||||
.api_client
|
||||
}
|
||||
|
||||
pub fn update_my_profile(&self) {
|
||||
let backend_hostname_ = self.backend_hostname.clone();
|
||||
|
@ -50,7 +59,8 @@ pub fn App() -> impl IntoView {
|
|||
|
||||
provide_meta_context();
|
||||
let backend_hostname = GlobalState {
|
||||
backend_hostname,
|
||||
backend_hostname: backend_hostname.clone(),
|
||||
api_client: ApiClient::new(Client::new(), backend_hostname.clone()),
|
||||
my_profile: None,
|
||||
};
|
||||
// Load user profile in case we are already logged in
|
||||
|
|
|
@ -7,7 +7,6 @@ use leptos_router::*;
|
|||
#[component]
|
||||
pub fn Nav() -> impl IntoView {
|
||||
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
||||
// TODO: use `<Show when` based on auth token for login/register/logout
|
||||
view! {
|
||||
<nav class="inner">
|
||||
<li>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
pub mod api;
|
||||
pub mod app;
|
||||
mod components;
|
||||
mod error;
|
||||
pub mod error;
|
||||
mod pages;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::frontend::api::get_article;
|
||||
use crate::common::GetArticleData;
|
||||
use crate::frontend::app::GlobalState;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
|
@ -13,7 +14,16 @@ pub fn Article() -> impl IntoView {
|
|||
.cloned()
|
||||
.unwrap_or("Main Page".to_string())
|
||||
},
|
||||
move |title| async move { get_article("localhost:8131", title).await.unwrap() },
|
||||
move |title| async move {
|
||||
GlobalState::api_client()
|
||||
.get_article(GetArticleData {
|
||||
title: Some(title),
|
||||
instance_id: None,
|
||||
id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
view! {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::common::LoginUserData;
|
||||
use crate::frontend::api::login;
|
||||
use crate::frontend::app::GlobalState;
|
||||
use crate::frontend::components::credentials::*;
|
||||
use leptos::*;
|
||||
|
@ -17,7 +16,7 @@ pub fn Login() -> impl IntoView {
|
|||
let credentials = LoginUserData { username, password };
|
||||
async move {
|
||||
set_wait_for_response.update(|w| *w = true);
|
||||
let result = login(&GlobalState::read_hostname(), credentials).await;
|
||||
let result = GlobalState::api_client().login(credentials).await;
|
||||
set_wait_for_response.update(|w| *w = false);
|
||||
match result {
|
||||
Ok(res) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::common::RegisterUserData;
|
||||
use crate::frontend::api::register;
|
||||
use crate::common::{LocalUserView, RegisterUserData};
|
||||
use crate::frontend::app::GlobalState;
|
||||
use crate::frontend::components::credentials::*;
|
||||
use crate::frontend::error::MyResult;
|
||||
use leptos::{logging::log, *};
|
||||
|
||||
#[component]
|
||||
|
@ -17,7 +17,8 @@ pub fn Register() -> impl IntoView {
|
|||
log!("Try to register new account for {}", credentials.username);
|
||||
async move {
|
||||
set_wait_for_response.update(|w| *w = true);
|
||||
let result = register(&GlobalState::read_hostname(), credentials).await;
|
||||
let result: MyResult<LocalUserView> =
|
||||
GlobalState::api_client().register(credentials).await;
|
||||
set_wait_for_response.update(|w| *w = false);
|
||||
match result {
|
||||
Ok(res) => {
|
||||
|
|
150
tests/common.rs
150
tests/common.rs
|
@ -1,23 +1,25 @@
|
|||
use anyhow::anyhow;
|
||||
use ibis::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
||||
use ibis::backend::api::instance::FollowInstance;
|
||||
use ibis::backend::api::user::AUTH_COOKIE;
|
||||
use ibis::backend::api::ResolveObject;
|
||||
use ibis::backend::database::conflict::ApiConflict;
|
||||
use ibis::backend::database::instance::DbInstance;
|
||||
use ibis::backend::error::MyResult;
|
||||
use ibis::backend::start;
|
||||
use ibis::common::ArticleView;
|
||||
use ibis::common::LoginUserData;
|
||||
use ibis::common::RegisterUserData;
|
||||
use ibis::frontend::api::{get_article, get_query, handle_json_res, register};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Client, ClientBuilder, StatusCode};
|
||||
use ibis_lib::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
||||
use ibis_lib::backend::api::instance::FollowInstance;
|
||||
use ibis_lib::backend::api::ResolveObject;
|
||||
use ibis_lib::backend::database::conflict::ApiConflict;
|
||||
use ibis_lib::backend::database::instance::DbInstance;
|
||||
use ibis_lib::backend::start;
|
||||
use ibis_lib::common::RegisterUserData;
|
||||
use ibis_lib::common::{ArticleView, GetArticleData};
|
||||
use ibis_lib::frontend::api::ApiClient;
|
||||
use ibis_lib::frontend::api::{get_query, handle_json_res};
|
||||
use ibis_lib::frontend::error::MyResult;
|
||||
|
||||
use reqwest::cookie::Jar;
|
||||
use reqwest::{ClientBuilder, StatusCode};
|
||||
use serde::de::Deserialize;
|
||||
use std::env::current_dir;
|
||||
use std::fs::create_dir_all;
|
||||
use std::ops::Deref;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Once;
|
||||
use std::thread::{sleep, spawn};
|
||||
use std::time::Duration;
|
||||
|
@ -25,8 +27,6 @@ use tokio::task::JoinHandle;
|
|||
use tracing::log::LevelFilter;
|
||||
use url::Url;
|
||||
|
||||
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
|
||||
|
||||
pub struct TestData {
|
||||
pub alpha: IbisInstance,
|
||||
pub beta: IbisInstance,
|
||||
|
@ -95,9 +95,7 @@ fn generate_db_path(name: &'static str, port: i32) -> String {
|
|||
}
|
||||
|
||||
pub struct IbisInstance {
|
||||
pub hostname: String,
|
||||
pub client: Client,
|
||||
pub jwt: String,
|
||||
pub api_client: ApiClient,
|
||||
db_path: String,
|
||||
db_handle: JoinHandle<()>,
|
||||
}
|
||||
|
@ -127,14 +125,20 @@ impl IbisInstance {
|
|||
username: username.to_string(),
|
||||
password: "hunter2".to_string(),
|
||||
};
|
||||
// TODO: use a separate http client for each backend instance, with cookie store for auth
|
||||
// TODO: how to pass the client/hostname to api client methods?
|
||||
// probably create a struct ApiClient(hostname, client) with all api methods in impl
|
||||
let client = ClientBuilder::new().cookie_store(true).build();
|
||||
let register_res = register(&hostname, form).await.unwrap();
|
||||
// use a separate http client for each backend instance, with cookie store for auth
|
||||
// how to pass the client/hostname to api client methods?
|
||||
// probably create a struct ApiClient(hostname, client) with all api methods in impl
|
||||
// TODO: seems that cookie isnt being stored? or maybe wrong hostname?
|
||||
let jar = Arc::new(Jar::default());
|
||||
let client = ClientBuilder::new()
|
||||
.cookie_store(true)
|
||||
.cookie_provider(jar.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
let api_client = ApiClient::new(client, hostname.clone());
|
||||
api_client.register(form).await.unwrap();
|
||||
Self {
|
||||
hostname,
|
||||
client,
|
||||
api_client,
|
||||
db_path,
|
||||
db_handle: handle,
|
||||
}
|
||||
|
@ -153,16 +157,27 @@ impl IbisInstance {
|
|||
}
|
||||
}
|
||||
|
||||
impl Deref for IbisInstance {
|
||||
type Target = ApiClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.api_client
|
||||
}
|
||||
}
|
||||
pub const TEST_ARTICLE_DEFAULT_TEXT: &str = "some\nexample\ntext\n";
|
||||
|
||||
pub async fn create_article(instance: &IbisInstance, title: String) -> MyResult<ArticleView> {
|
||||
let create_form = CreateArticleData {
|
||||
title: title.clone(),
|
||||
};
|
||||
let req = CLIENT
|
||||
.post(format!("http://{}/api/v1/article", &instance.hostname))
|
||||
.form(&create_form)
|
||||
.bearer_auth(&instance.jwt);
|
||||
let req = instance
|
||||
.api_client
|
||||
.client
|
||||
.post(format!(
|
||||
"http://{}/api/v1/article",
|
||||
&instance.api_client.hostname
|
||||
))
|
||||
.form(&create_form);
|
||||
let article: ArticleView = handle_json_res(req).await?;
|
||||
|
||||
// create initial edit to ensure that conflicts are generated (there are no conflicts on empty file)
|
||||
|
@ -172,28 +187,30 @@ pub async fn create_article(instance: &IbisInstance, title: String) -> MyResult<
|
|||
previous_version_id: article.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
edit_article(instance, &edit_form).await
|
||||
Ok(edit_article(instance, &edit_form).await.unwrap())
|
||||
}
|
||||
|
||||
pub async fn edit_article_with_conflict(
|
||||
instance: &IbisInstance,
|
||||
edit_form: &EditArticleData,
|
||||
) -> MyResult<Option<ApiConflict>> {
|
||||
let req = CLIENT
|
||||
.patch(format!("http://{}/api/v1/article", instance.hostname))
|
||||
.form(edit_form)
|
||||
.bearer_auth(&instance.jwt);
|
||||
handle_json_res(req).await?
|
||||
let req = instance
|
||||
.api_client
|
||||
.client
|
||||
.patch(format!(
|
||||
"http://{}/api/v1/article",
|
||||
instance.api_client.hostname
|
||||
))
|
||||
.form(edit_form);
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn get_conflicts(instance: &IbisInstance) -> MyResult<Vec<ApiConflict>> {
|
||||
let req = CLIENT
|
||||
.get(format!(
|
||||
"http://{}/api/v1/edit_conflicts",
|
||||
&instance.hostname
|
||||
))
|
||||
.bearer_auth(&instance.jwt);
|
||||
handle_json_res(req).await?
|
||||
let req = instance.api_client.client.get(format!(
|
||||
"http://{}/api/v1/edit_conflicts",
|
||||
&instance.api_client.hostname
|
||||
));
|
||||
Ok(handle_json_res(req).await.unwrap())
|
||||
}
|
||||
|
||||
pub async fn edit_article(
|
||||
|
@ -203,51 +220,70 @@ pub async fn edit_article(
|
|||
let edit_res = edit_article_with_conflict(instance, edit_form).await?;
|
||||
assert!(edit_res.is_none());
|
||||
|
||||
get_article(&instance.hostname, todo!("{}", edit_form.article_id)).await
|
||||
instance
|
||||
.api_client
|
||||
.get_article(GetArticleData {
|
||||
title: None,
|
||||
instance_id: None,
|
||||
id: Some(edit_form.article_id),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get<T>(hostname: &str, endpoint: &str) -> MyResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
get_query(hostname, endpoint, None::<i32>).await
|
||||
Ok(get_query(hostname, endpoint, None::<i32>).await.unwrap())
|
||||
}
|
||||
|
||||
pub async fn fork_article(
|
||||
instance: &IbisInstance,
|
||||
form: &ForkArticleData,
|
||||
) -> MyResult<ArticleView> {
|
||||
let req = CLIENT
|
||||
.post(format!("http://{}/api/v1/article/fork", instance.hostname))
|
||||
.form(form)
|
||||
.bearer_auth(&instance.jwt);
|
||||
handle_json_res(req).await?
|
||||
let req = instance
|
||||
.api_client
|
||||
.client
|
||||
.post(format!(
|
||||
"http://{}/api/v1/article/fork",
|
||||
instance.api_client.hostname
|
||||
))
|
||||
.form(form);
|
||||
Ok(handle_json_res(req).await.unwrap())
|
||||
}
|
||||
|
||||
pub async fn follow_instance(instance: &IbisInstance, follow_instance: &str) -> MyResult<()> {
|
||||
pub async fn follow_instance(
|
||||
instance: &IbisInstance,
|
||||
follow_instance: &str,
|
||||
) -> MyResult<DbInstance> {
|
||||
// fetch beta instance on alpha
|
||||
let resolve_form = ResolveObject {
|
||||
id: Url::parse(&format!("http://{}", follow_instance))?,
|
||||
};
|
||||
let instance_resolved: DbInstance =
|
||||
get_query(&instance.hostname, "resolve_instance", Some(resolve_form)).await?;
|
||||
let instance_resolved: DbInstance = get_query(
|
||||
&instance.api_client.hostname,
|
||||
"instance/resolve",
|
||||
Some(resolve_form),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// send follow
|
||||
let follow_form = FollowInstance {
|
||||
id: instance_resolved.id,
|
||||
};
|
||||
// cant use post helper because follow doesnt return json
|
||||
let res = CLIENT
|
||||
let res = instance
|
||||
.api_client
|
||||
.client
|
||||
.post(format!(
|
||||
"http://{}/api/v1/instance/follow",
|
||||
instance.hostname
|
||||
instance.api_client.hostname
|
||||
))
|
||||
.form(&follow_form)
|
||||
.bearer_auth(&instance.jwt)
|
||||
.send()
|
||||
.await?;
|
||||
if res.status() == StatusCode::OK {
|
||||
Ok(())
|
||||
Ok(instance_resolved)
|
||||
} else {
|
||||
Err(anyhow!("API error: {}", res.text().await?).into())
|
||||
}
|
||||
|
|
155
tests/test.rs
155
tests/test.rs
|
@ -1,4 +1,4 @@
|
|||
extern crate ibis;
|
||||
extern crate ibis_lib;
|
||||
|
||||
mod common;
|
||||
|
||||
|
@ -6,23 +6,21 @@ use crate::common::fork_article;
|
|||
use crate::common::get_conflicts;
|
||||
use crate::common::{
|
||||
create_article, edit_article, edit_article_with_conflict, follow_instance, get, TestData,
|
||||
CLIENT, TEST_ARTICLE_DEFAULT_TEXT,
|
||||
TEST_ARTICLE_DEFAULT_TEXT,
|
||||
};
|
||||
use ibis::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
||||
use ibis::backend::api::{ResolveObject, SearchArticleData};
|
||||
use ibis::backend::database::instance::{DbInstance, InstanceView};
|
||||
use ibis::backend::error::MyResult;
|
||||
use ibis::common::ArticleView;
|
||||
use ibis::common::DbArticle;
|
||||
use ibis::frontend::api::get_article;
|
||||
use ibis::frontend::api::get_query;
|
||||
use ibis::frontend::api::handle_json_res;
|
||||
use ibis::frontend::api::login;
|
||||
use ibis_lib::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
||||
use ibis_lib::backend::api::{ResolveObject, SearchArticleData};
|
||||
use ibis_lib::backend::database::instance::{DbInstance, InstanceView};
|
||||
use ibis_lib::common::{ArticleView, GetArticleData};
|
||||
use ibis_lib::common::{DbArticle, LoginUserData, RegisterUserData};
|
||||
use ibis_lib::frontend::api::get_query;
|
||||
use ibis_lib::frontend::api::handle_json_res;
|
||||
use ibis_lib::frontend::error::MyResult;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use url::Url;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_read_and_edit_article() -> MyResult<()> {
|
||||
async fn test_create_read_and_edit_local_article() -> MyResult<()> {
|
||||
let data = TestData::start().await;
|
||||
|
||||
// create article
|
||||
|
@ -32,13 +30,22 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
|
|||
assert!(create_res.article.local);
|
||||
|
||||
// now article can be read
|
||||
let get_res = get_article(&data.alpha.hostname, create_res.article.id).await?;
|
||||
let get_article_data = GetArticleData {
|
||||
title: Some(create_res.article.title.clone()),
|
||||
instance_id: None,
|
||||
id: None,
|
||||
};
|
||||
let get_res = data
|
||||
.alpha
|
||||
.get_article(get_article_data.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(title, get_res.article.title);
|
||||
assert_eq!(TEST_ARTICLE_DEFAULT_TEXT, get_res.article.text);
|
||||
assert!(get_res.article.local);
|
||||
|
||||
// error on article which wasnt federated
|
||||
let not_found = get_article(&data.beta.hostname, create_res.article.id).await;
|
||||
let not_found = data.beta.get_article(get_article_data.clone()).await;
|
||||
assert!(not_found.is_err());
|
||||
|
||||
// edit article
|
||||
|
@ -56,7 +63,9 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
|
|||
query: title.clone(),
|
||||
};
|
||||
let search_res: Vec<DbArticle> =
|
||||
get_query(&data.alpha.hostname, "search", Some(search_form)).await?;
|
||||
get_query(&data.alpha.api_client.hostname, "search", Some(search_form))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(1, search_res.len());
|
||||
assert_eq!(edit_res.article, search_res[0]);
|
||||
|
||||
|
@ -134,23 +143,30 @@ async fn test_synchronize_articles() -> MyResult<()> {
|
|||
};
|
||||
edit_article(&data.alpha, &edit_form).await?;
|
||||
|
||||
// article is not yet on beta
|
||||
let get_res = get_article(&data.beta.hostname, create_res.article.id).await;
|
||||
assert!(get_res.is_err());
|
||||
|
||||
// fetch alpha instance on beta, articles are also fetched automatically
|
||||
let resolve_object = ResolveObject {
|
||||
id: Url::parse(&format!("http://{}", &data.alpha.hostname))?,
|
||||
};
|
||||
get_query::<DbInstance, _>(
|
||||
let instance: DbInstance = get_query::<DbInstance, _>(
|
||||
&data.beta.hostname,
|
||||
"resolve_instance",
|
||||
"instance/resolve",
|
||||
Some(resolve_object),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the article and compare
|
||||
let get_res = get_article(&data.beta.hostname, create_res.article.id).await?;
|
||||
let mut get_article_data = GetArticleData {
|
||||
title: Some(create_res.article.title),
|
||||
instance_id: None,
|
||||
id: None,
|
||||
};
|
||||
|
||||
// try to read remote article by name, fails without domain
|
||||
let get_res = data.beta.get_article(get_article_data.clone()).await;
|
||||
assert!(get_res.is_err());
|
||||
|
||||
// get the article with instance id and compare
|
||||
get_article_data.instance_id = Some(instance.id);
|
||||
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!(title, get_res.article.title);
|
||||
assert_eq!(2, get_res.edits.len());
|
||||
|
@ -164,7 +180,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
|
|||
async fn test_edit_local_article() -> MyResult<()> {
|
||||
let data = TestData::start().await;
|
||||
|
||||
follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
let beta_instance = follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
|
||||
// create new article
|
||||
let title = "Manu_Chao".to_string();
|
||||
|
@ -173,7 +189,12 @@ async fn test_edit_local_article() -> MyResult<()> {
|
|||
assert!(create_res.article.local);
|
||||
|
||||
// article should be federated to alpha
|
||||
let get_res = get_article(&data.alpha.hostname, create_res.article.id).await?;
|
||||
let get_article_data = GetArticleData {
|
||||
title: Some(create_res.article.title.to_string()),
|
||||
instance_id: Some(beta_instance.id),
|
||||
id: None,
|
||||
};
|
||||
let get_res = data.alpha.get_article(get_article_data.clone()).await?;
|
||||
assert_eq!(create_res.article.title, get_res.article.title);
|
||||
assert_eq!(1, get_res.edits.len());
|
||||
assert!(!get_res.article.local);
|
||||
|
@ -195,7 +216,7 @@ async fn test_edit_local_article() -> MyResult<()> {
|
|||
.starts_with(&edit_res.article.ap_id.to_string()));
|
||||
|
||||
// edit should be federated to alpha
|
||||
let get_res = get_article(&data.alpha.hostname, edit_res.article.id).await?;
|
||||
let get_res = data.alpha.get_article(get_article_data).await?;
|
||||
assert_eq!(edit_res.article.title, get_res.article.title);
|
||||
assert_eq!(edit_res.edits.len(), 2);
|
||||
assert_eq!(edit_res.article.text, get_res.article.text);
|
||||
|
@ -207,27 +228,43 @@ async fn test_edit_local_article() -> MyResult<()> {
|
|||
async fn test_edit_remote_article() -> MyResult<()> {
|
||||
let data = TestData::start().await;
|
||||
|
||||
follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
follow_instance(&data.gamma, &data.beta.hostname).await?;
|
||||
let beta_id_on_alpha = follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
let beta_id_on_gamma = follow_instance(&data.gamma, &data.beta.hostname).await?;
|
||||
|
||||
// create new article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.beta, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert_eq!(&title, &create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
// article should be federated to alpha and gamma
|
||||
let get_res = get_article(&data.alpha.hostname, create_res.article.id).await?;
|
||||
let get_article_data_alpha = GetArticleData {
|
||||
title: Some(create_res.article.title.to_string()),
|
||||
instance_id: Some(beta_id_on_alpha.id),
|
||||
id: None,
|
||||
};
|
||||
let get_res = data
|
||||
.alpha
|
||||
.get_article(get_article_data_alpha.clone())
|
||||
.await?;
|
||||
assert_eq!(create_res.article.title, get_res.article.title);
|
||||
assert_eq!(1, get_res.edits.len());
|
||||
assert!(!get_res.article.local);
|
||||
|
||||
let get_res = get_article(&data.gamma.hostname, create_res.article.id).await?;
|
||||
let get_article_data_gamma = GetArticleData {
|
||||
title: Some(create_res.article.title.to_string()),
|
||||
instance_id: Some(beta_id_on_gamma.id),
|
||||
id: None,
|
||||
};
|
||||
let get_res = data
|
||||
.gamma
|
||||
.get_article(get_article_data_gamma.clone())
|
||||
.await?;
|
||||
assert_eq!(create_res.article.title, get_res.article.title);
|
||||
assert_eq!(create_res.article.text, get_res.article.text);
|
||||
|
||||
let edit_form = EditArticleData {
|
||||
article_id: create_res.article.id,
|
||||
article_id: get_res.article.id,
|
||||
new_text: "Lorem Ipsum 2".to_string(),
|
||||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
|
@ -242,12 +279,12 @@ async fn test_edit_remote_article() -> MyResult<()> {
|
|||
.starts_with(&edit_res.article.ap_id.to_string()));
|
||||
|
||||
// edit should be federated to beta and gamma
|
||||
let get_res = get_article(&data.alpha.hostname, create_res.article.id).await?;
|
||||
let get_res = data.alpha.get_article(get_article_data_alpha).await?;
|
||||
assert_eq!(edit_res.article.title, get_res.article.title);
|
||||
assert_eq!(edit_res.edits.len(), 2);
|
||||
assert_eq!(edit_res.article.text, get_res.article.text);
|
||||
|
||||
let get_res = get_article(&data.gamma.hostname, create_res.article.id).await?;
|
||||
let get_res = data.gamma.get_article(get_article_data_gamma).await?;
|
||||
assert_eq!(edit_res.article.title, get_res.article.title);
|
||||
assert_eq!(edit_res.edits.len(), 2);
|
||||
assert_eq!(edit_res.article.text, get_res.article.text);
|
||||
|
@ -311,7 +348,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
|||
async fn test_federated_edit_conflict() -> MyResult<()> {
|
||||
let data = TestData::start().await;
|
||||
|
||||
follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
let beta_id_on_alpha = follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
|
||||
// create new article
|
||||
let title = "Manu_Chao".to_string();
|
||||
|
@ -325,15 +362,23 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
|||
};
|
||||
let resolve_res: ArticleView = get_query(
|
||||
&data.gamma.hostname,
|
||||
"resolve_article",
|
||||
"article/resolve",
|
||||
Some(resolve_object),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(create_res.article.text, resolve_res.article.text);
|
||||
|
||||
// alpha edits article
|
||||
let get_article_data = GetArticleData {
|
||||
title: Some(title.to_string()),
|
||||
instance_id: Some(beta_id_on_alpha.id),
|
||||
id: None,
|
||||
};
|
||||
let get_res = data.alpha.get_article(get_article_data).await?;
|
||||
assert_eq!(&create_res.edits.len(), &get_res.edits.len());
|
||||
assert_eq!(&create_res.edits[0].hash, &get_res.edits[0].hash);
|
||||
let edit_form = EditArticleData {
|
||||
article_id: create_res.article.id,
|
||||
article_id: get_res.article.id,
|
||||
new_text: "Lorem Ipsum\n".to_string(),
|
||||
previous_version_id: create_res.latest_version.clone(),
|
||||
resolve_conflict_id: None,
|
||||
|
@ -350,7 +395,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
|||
// gamma also edits, as its not the latest version there is a conflict. local version should
|
||||
// not be updated with this conflicting version, instead user needs to handle the conflict
|
||||
let edit_form = EditArticleData {
|
||||
article_id: create_res.article.id,
|
||||
article_id: resolve_res.article.id,
|
||||
new_text: "aaaa\n".to_string(),
|
||||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
|
@ -365,7 +410,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
|||
|
||||
// resolve the conflict
|
||||
let edit_form = EditArticleData {
|
||||
article_id: create_res.article.id,
|
||||
article_id: resolve_res.article.id,
|
||||
new_text: "aaaa\n".to_string(),
|
||||
previous_version_id: conflicts[0].previous_version_id.clone(),
|
||||
resolve_conflict_id: Some(conflicts[0].id.clone()),
|
||||
|
@ -432,7 +477,7 @@ async fn test_fork_article() -> MyResult<()> {
|
|||
id: create_res.article.ap_id.into_inner(),
|
||||
};
|
||||
let resolve_res: ArticleView =
|
||||
get_query(&data.beta.hostname, "resolve_article", Some(resolve_object)).await?;
|
||||
get_query(&data.beta.hostname, "article/resolve", Some(resolve_object)).await?;
|
||||
let resolved_article = resolve_res.article;
|
||||
assert_eq!(create_res.edits.len(), resolve_res.edits.len());
|
||||
|
||||
|
@ -471,24 +516,36 @@ async fn test_user_registration_login() -> MyResult<()> {
|
|||
let data = TestData::start().await;
|
||||
let username = "my_user";
|
||||
let password = "hunter2";
|
||||
let register = register(&data.alpha.hostname, username, password).await?;
|
||||
assert!(!register.jwt.is_empty());
|
||||
let register_data = RegisterUserData {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
data.alpha.register(register_data).await?;
|
||||
|
||||
let invalid_login = login(&data.alpha, username, "asd123").await;
|
||||
let login_data = LoginUserData {
|
||||
username: username.to_string(),
|
||||
password: "asd123".to_string(),
|
||||
};
|
||||
let invalid_login = data.alpha.login(login_data).await;
|
||||
assert!(invalid_login.is_err());
|
||||
|
||||
let valid_login = login(&data.alpha, username, password).await?;
|
||||
assert!(!valid_login.jwt.is_empty());
|
||||
let login_data = LoginUserData {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
data.alpha.login(login_data).await?;
|
||||
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_form = CreateArticleData {
|
||||
title: title.clone(),
|
||||
};
|
||||
|
||||
let req = CLIENT
|
||||
let req = data
|
||||
.alpha
|
||||
.api_client
|
||||
.client
|
||||
.post(format!("http://{}/api/v1/article", &data.alpha.hostname))
|
||||
.form(&create_form)
|
||||
.bearer_auth(valid_login.jwt);
|
||||
.form(&create_form);
|
||||
let create_res: ArticleView = handle_json_res(req).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
|
||||
|
|
Loading…
Reference in a new issue