From 84021a8ffc481cbcc53ba46d8fd9e66913e29466 Mon Sep 17 00:00:00 2001 From: Esteban Borai Date: Sat, 2 Dec 2023 14:42:37 -0800 Subject: [PATCH] feat: api error codes and redacted secrets (#6) Provides `Secret` struct to keep sensitive data hidden, also Error Codes support. Makes sure core logic has correct error handling by enumerating errors from different cases and providing explicit Status Codes for each error. Makes sure routes matches the ones under `account`, refer: https://github.com/commune-os/commune-server/blob/df257384c8daede9fdf0e93c51af3c6444e15745/app/routes.go#L122-L164 --- Cargo.toml | 1 + crates/core/Cargo.toml | 6 +- crates/core/src/error.rs | 41 +++++ crates/core/src/lib.rs | 18 +- crates/core/src/user/error.rs | 29 +++ crates/core/src/user/mod.rs | 1 + crates/core/src/user/service.rs | 171 ++++++++++++++++-- crates/core/src/util/mod.rs | 1 + crates/core/src/util/secret.rs | 87 +++++++++ crates/core/src/util/time.rs | 11 +- crates/matrix/Cargo.toml | 1 + crates/matrix/src/admin/resources/user.rs | 56 ++++-- crates/server/src/bin/main.rs | 2 + crates/server/src/router/api/mod.rs | 22 ++- .../{user/register.rs => account/create.rs} | 4 +- .../router/api/v1/{user => account}/mod.rs | 8 +- crates/server/src/router/api/v1/mod.rs | 4 +- .../{user/register.rs => account/create.rs} | 4 +- crates/test/src/server/api/v1/account/mod.rs | 1 + crates/test/src/server/api/v1/mod.rs | 2 +- crates/test/src/server/api/v1/user/mod.rs | 1 - 21 files changed, 425 insertions(+), 46 deletions(-) create mode 100644 crates/core/src/error.rs create mode 100644 crates/core/src/user/error.rs create mode 100644 crates/core/src/util/secret.rs rename crates/server/src/router/api/v1/{user/register.rs => account/create.rs} (93%) rename crates/server/src/router/api/v1/{user => account}/mod.rs (55%) rename crates/test/src/server/api/v1/{user/register.rs => account/create.rs} (85%) create mode 100644 crates/test/src/server/api/v1/account/mod.rs delete mode 100644 crates/test/src/server/api/v1/user/mod.rs diff --git a/Cargo.toml b/Cargo.toml index a2acfab..45e50e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ resolver = "1" anyhow = "1.0.75" axum = "0.6.19" dotenv = "0.15.0" +http = "0.2.11" reqwest = "0.11.22" serde = "1.0.192" tokio = "1.34.0" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 020d4dc..20fd431 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -9,10 +9,14 @@ name = "commune" path = "src/lib.rs" [dependencies] +thiserror = "1.0.50" validator = { version = "0.16", features = ["derive"] } # Workspace -anyhow = { workspace = true } +http = { workspace = true } +serde = { workspace = true, features = ["derive"] } +tracing = { workspace = true } +url = { workspace = true, features = ["serde"] } # Local Dependencies matrix = { path = "../matrix" } diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs new file mode 100644 index 0000000..f29b621 --- /dev/null +++ b/crates/core/src/error.rs @@ -0,0 +1,41 @@ +use http::StatusCode; +use thiserror::Error; + +use crate::user::error::UserErrorCode; + +pub type Result = std::result::Result; + +pub trait HttpStatusCode { + fn status_code(&self) -> StatusCode; + fn error_code(&self) -> &'static str; +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("User Error. {0}")] + User(UserErrorCode), + #[error("Unknown Error Occured")] + Unknown, +} + +impl From for Error { + fn from(err: UserErrorCode) -> Self { + Error::User(err) + } +} + +impl HttpStatusCode for Error { + fn status_code(&self) -> StatusCode { + match self { + Error::User(err) => err.status_code(), + Error::Unknown => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_code(&self) -> &'static str { + match self { + Error::User(err) => err.error_code(), + Error::Unknown => "UNKNOWN_ERROR", + } + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 1029fce..5f1fdcf 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,9 +1,10 @@ +pub mod error; pub mod user; pub mod util; -use std::fmt::Debug; +pub use error::{Error, HttpStatusCode, Result}; -use anyhow::Result; +use std::fmt::Debug; use matrix::admin::Client as MatrixAdminClient; @@ -24,9 +25,16 @@ pub struct Commune { impl Commune { pub fn new>(config: C) -> Result { let config: CommuneConfig = config.into(); - let mut admin = MatrixAdminClient::new(config.synapse_host, config.synapse_server_name)?; - - admin.set_token(config.synapse_admin_token)?; + let mut admin = MatrixAdminClient::new(config.synapse_host, config.synapse_server_name) + .map_err(|err| { + tracing::error!(?err, "Failed to create admin client"); + Error::Unknown + })?; + + admin.set_token(config.synapse_admin_token).map_err(|err| { + tracing::error!(?err, "Failed to set admin token"); + Error::Unknown + })?; Ok(Self { user: UserService::new(admin), diff --git a/crates/core/src/user/error.rs b/crates/core/src/user/error.rs new file mode 100644 index 0000000..7b1a270 --- /dev/null +++ b/crates/core/src/user/error.rs @@ -0,0 +1,29 @@ +use http::StatusCode; +use thiserror::Error; +use validator::ValidationErrors; + +use crate::error::HttpStatusCode; + +#[derive(Debug, Error)] +pub enum UserErrorCode { + #[error("Vaildation error. {0}")] + ValidationError(#[from] ValidationErrors), + #[error("The username {0} is already taken")] + UsernameTaken(String), +} + +impl HttpStatusCode for UserErrorCode { + fn status_code(&self) -> StatusCode { + match self { + UserErrorCode::ValidationError(_) => StatusCode::BAD_REQUEST, + UserErrorCode::UsernameTaken(_) => StatusCode::CONFLICT, + } + } + + fn error_code(&self) -> &'static str { + match self { + UserErrorCode::ValidationError(_) => "VALIDATION_ERROR", + UserErrorCode::UsernameTaken(_) => "USERNAME_TAKEN", + } + } +} diff --git a/crates/core/src/user/mod.rs b/crates/core/src/user/mod.rs index 908db95..e1001c6 100644 --- a/crates/core/src/user/mod.rs +++ b/crates/core/src/user/mod.rs @@ -1,2 +1,3 @@ +pub mod error; pub mod model; pub mod service; diff --git a/crates/core/src/user/service.rs b/crates/core/src/user/service.rs index 20f9257..9a73ff9 100644 --- a/crates/core/src/user/service.rs +++ b/crates/core/src/user/service.rs @@ -1,26 +1,76 @@ -use anyhow::{bail, Result}; -use validator::Validate; +use tracing::instrument; +use url::Url; +use validator::{Validate, ValidationError}; -use matrix::admin::resources::user::{ThreePid, User as MatrixUser, UserCreateDto}; +use matrix::admin::resources::user::{ + ListUsersParams, ThreePid, User as MatrixUser, UserCreateDto, +}; use matrix::admin::resources::user_id::UserId; use matrix::admin::Client as MatrixAdminClient; +use crate::util::secret::Secret; use crate::util::time::timestamp; +use crate::{Error, Result}; +use super::error::UserErrorCode; use super::model::User; +const DEFAULT_AVATAR_URL: &str = "https://via.placeholder.com/150"; +const MIN_USERNAME_LENGTH: usize = 3; +const MAX_USERNAME_LENGTH: usize = 12; +const MIN_PASSWORD_LENGTH: usize = 8; + +pub struct LoginDto { + pub username: String, + pub password: String, +} + #[derive(Debug, Validate)] pub struct CreateAccountDto { - #[validate(length(min = 8, max = 12))] + #[validate(custom = "CreateAccountDto::validate_username")] pub username: String, - #[validate(length(min = 8, max = 12))] - pub password: String, + #[validate(custom = "CreateAccountDto::validate_password")] + pub password: Secret, #[validate(email)] pub email: String, pub session: String, pub code: String, } +impl CreateAccountDto { + /// Validation logic for usernames enforced in user creation + fn validate_username(username: &str) -> std::result::Result<(), ValidationError> { + if username.len() < MIN_USERNAME_LENGTH { + return Err(ValidationError::new("username is too short")); + } + + if username.len() > MAX_USERNAME_LENGTH { + return Err(ValidationError::new("username is too long")); + } + + if username.contains(' ') { + return Err(ValidationError::new("username cannot contain spaces")); + } + + if username.to_ascii_lowercase() != username { + return Err(ValidationError::new( + "username cannot contain uppercase letters", + )); + } + + Ok(()) + } + + /// Validation logic for passwords enforced in user creation + fn validate_password(password: &Secret) -> std::result::Result<(), ValidationError> { + if password.inner().len() < MIN_PASSWORD_LENGTH { + return Err(ValidationError::new("password is too short")); + } + + Ok(()) + } +} + pub struct UserService { admin: MatrixAdminClient, } @@ -30,18 +80,44 @@ impl UserService { Self { admin } } + #[instrument(skip(self, dto))] pub async fn register(&self, dto: CreateAccountDto) -> Result { - dto.validate()?; + dto.validate().map_err(|err| { + tracing::warn!(?err, "Failed to validate user creation dto"); + UserErrorCode::from(err) + })?; let user_id = UserId::new(dto.username.clone(), self.admin.server_name().to_string()); + let exists = MatrixUser::list( + &self.admin, + ListUsersParams { + user_id: Some(user_id.to_string()), + ..Default::default() + }, + ) + .await + .map_err(|err| { + tracing::error!(?err, "Failed to list users"); + Error::Unknown + })?; + + if !exists.users.is_empty() { + return Err(UserErrorCode::UsernameTaken(dto.username).into()); + } + + let avatar_url = Url::parse(DEFAULT_AVATAR_URL).map_err(|err| { + tracing::error!(?err, "Failed to parse default avatar url"); + Error::Unknown + })?; + let matrix_user = MatrixUser::create( &self.admin, user_id, UserCreateDto { displayname: Some(dto.username), - password: dto.password, + password: dto.password.to_string(), logout_devices: false, - avatar_url: None, + avatar_url: Some(avatar_url), threepids: vec![ThreePid { medium: "email".to_string(), address: dto.email, @@ -55,14 +131,20 @@ impl UserService { locked: false, }, ) - .await?; + .await + .map_err(|err| { + tracing::error!(?err, "Failed to create user"); + Error::Unknown + })?; let Some(displayname) = matrix_user.displayname else { - bail!("Matrix displayname is empty, this value cannot be empty"); + tracing::error!("Failed to get displayname for user"); + return Err(Error::Unknown); }; let Some(threepid) = matrix_user.threepids.first() else { - bail!("Matrix Threepid should exist, this value cannot be empty"); + tracing::error!("Failed to get threepid for user"); + return Err(Error::Unknown); }; Ok(User { @@ -73,3 +155,68 @@ impl UserService { }) } } + +#[cfg(test)] +mod test { + use validator::Validate; + + use crate::util::secret::Secret; + + use super::CreateAccountDto; + + #[test] + fn ensure_username_is_not_too_short() { + let dto = CreateAccountDto { + username: "ab".to_string(), + password: Secret::new("password"), + email: "aby@mail.com".to_string(), + code: "1234".to_string(), + session: "synapse".to_string(), + }; + let err = dto.validate().err().unwrap(); + + assert_eq!(err.to_string(), "username is too short"); + } + + #[test] + fn ensure_username_is_not_too_long() { + let dto = CreateAccountDto { + username: "abbeyroadismyfavoritealbum".to_string(), + password: Secret::new("password"), + email: "aby@mail.com".to_string(), + code: "1234".to_string(), + session: "synapse".to_string(), + }; + let err = dto.validate().err().unwrap(); + + assert_eq!(err.to_string(), "username is too long"); + } + + #[test] + fn ensure_username_does_not_contain_spaces() { + let dto = CreateAccountDto { + username: "abbey road".to_string(), + password: Secret::new("password"), + email: "aby@mail.com".to_string(), + code: "1234".to_string(), + session: "synapse".to_string(), + }; + let err = dto.validate().err().unwrap(); + + assert_eq!(err.to_string(), "username cannot contain spaces"); + } + + #[test] + fn ensure_username_is_lowercased() { + let dto = CreateAccountDto { + username: "AbbeyRoad".to_string(), + password: Secret::new("password"), + email: "aby@mail.com".to_string(), + code: "1234".to_string(), + session: "synapse".to_string(), + }; + let err = dto.validate().err().unwrap(); + + assert_eq!(err.to_string(), "username cannot contain uppercase letters"); + } +} diff --git a/crates/core/src/util/mod.rs b/crates/core/src/util/mod.rs index 077885d..fe66b81 100644 --- a/crates/core/src/util/mod.rs +++ b/crates/core/src/util/mod.rs @@ -1 +1,2 @@ +pub mod secret; pub mod time; diff --git a/crates/core/src/util/secret.rs b/crates/core/src/util/secret.rs new file mode 100644 index 0000000..423c627 --- /dev/null +++ b/crates/core/src/util/secret.rs @@ -0,0 +1,87 @@ +use std::borrow::Cow; +use std::convert::Infallible; +use std::fmt::{Debug, Display}; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +/// A `String` wrapper that does not display the value when debugged or displayed. +#[derive(Clone, Deserialize, PartialEq, Eq, Hash, Serialize)] +pub struct Secret(Cow<'static, str>); + +impl Secret { + pub fn new(s: impl Into>) -> Self { + Secret(s.into()) + } + + #[inline] + pub fn inner(&self) -> &str { + &self.0 + } + + /// Returs inner value as [`String`] + /// + /// # Shadowing Note + /// + /// Intentially shadows [`std::string::ToString::to_string`] to prevent + /// getting `"[REDACTED]"` when using `to_string`. + #[allow(clippy::inherent_to_string_shadow_display)] + pub fn to_string(&self) -> String { + self.0.to_string() + } +} + +impl From for Secret { + fn from(s: String) -> Self { + Secret(Cow::Owned(s)) + } +} + +impl FromStr for Secret { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Secret(Cow::Owned(s.to_owned()))) + } +} + +impl Debug for Secret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("[REDACTED]") + } +} + +impl Display for Secret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("[REDACTED]") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn do_not_display_value() { + let secret = Secret::new("secret"); + let display = format!("{}", secret); + + assert_eq!(display, "[REDACTED]"); + } + + #[test] + fn do_not_debug_value() { + let secret = Secret::new("secret"); + let display = format!("{:?}", secret); + + assert_eq!(display, "[REDACTED]"); + } + + #[test] + fn retrieves_original() { + let secret = Secret::new("secret"); + let value = secret.inner(); + + assert_eq!(value, "secret"); + } +} diff --git a/crates/core/src/util/time.rs b/crates/core/src/util/time.rs index c685a60..06a62e0 100644 --- a/crates/core/src/util/time.rs +++ b/crates/core/src/util/time.rs @@ -1,8 +1,13 @@ -use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; -pub fn timestamp() -> Result { +use crate::error::Error; + +pub fn timestamp() -> Result { let start = SystemTime::now(); - let since_the_epoch = start.duration_since(UNIX_EPOCH)?; + let since_the_epoch = start.duration_since(UNIX_EPOCH).map_err(|err| { + tracing::error!(?err, "Failed to get timestamp"); + Error::Unknown + })?; Ok(since_the_epoch.as_secs()) } diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index 522bc2d..50ddad3 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -17,4 +17,5 @@ sha1 = "0.10.6" anyhow = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } +tracing = { workspace = true } url = { workspace = true, features = ["serde"] } diff --git a/crates/matrix/src/admin/resources/user.rs b/crates/matrix/src/admin/resources/user.rs index adeb61e..52841e0 100644 --- a/crates/matrix/src/admin/resources/user.rs +++ b/crates/matrix/src/admin/resources/user.rs @@ -5,6 +5,7 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; +use tracing::instrument; use url::Url; use crate::admin::Client; @@ -62,15 +63,39 @@ pub struct UserCreateDto { pub locked: bool, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct ListUsersParams { - user_id: Option, - name: Option, - guests: Option, - admins: Option, - deactivated: Option, - limit: Option, - from: Option, + pub user_id: Option, + pub name: Option, + pub guests: Option, + pub admins: Option, + pub deactivated: Option, + pub limit: Option, + pub from: Option, +} + +/// Data type for the response of the `GET /_synapse/admin/v2/users` endpoint. +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ListUser { + pub name: String, + pub user_type: Option, + pub is_guest: usize, + pub admin: usize, + pub deactivated: usize, + pub shadow_banned: bool, + pub avatar_url: Option, + pub creation_ts: u64, + pub last_seen_ts: Option, + pub erased: bool, + pub locked: bool, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ListUsersResponse { + pub users: Vec, + pub total: u64, + #[serde(default)] + pub next_token: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -88,9 +113,14 @@ pub struct UserUpdateDto { } impl User { - /// Allows an administrator to create a user account + /// Allows an administrator to create a user account. + /// + /// Note that internally Synapse uses this same endpoint to modify an + /// existing user account, so this method will modify the existing user + /// if [`UserId`] matches. /// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account + #[instrument(skip(client, dto))] pub async fn create(client: &Client, user_id: UserId, dto: UserCreateDto) -> Result { let resp = client .put_json( @@ -106,17 +136,21 @@ impl User { /// ascending user ID. /// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#list-accounts - pub async fn list(client: &Client, params: ListUsersParams) -> Result { + #[instrument(skip(client))] + pub async fn list(client: &Client, params: ListUsersParams) -> Result { let resp = client .get_query("/_synapse/admin/v2/users", ¶ms) .await?; + println!("{:?}", resp); + let data: ListUsersResponse = resp.json().await?; - Ok(resp.json().await?) + Ok(data) } /// Allows an administrator to modify a user account /// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account + #[instrument(skip(client))] pub async fn update(client: &Client, user_id: UserId, dto: UserUpdateDto) -> Result { let resp = client .put_json( diff --git a/crates/server/src/bin/main.rs b/crates/server/src/bin/main.rs index a790cd7..a29b855 100644 --- a/crates/server/src/bin/main.rs +++ b/crates/server/src/bin/main.rs @@ -14,6 +14,8 @@ async fn main() -> Result<()> { let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); let tcp = TcpListener::bind(addr)?; + tracing::info!("Listening on {}", addr); + commune_server::serve(tcp).await?; Ok(()) diff --git a/crates/server/src/router/api/mod.rs b/crates/server/src/router/api/mod.rs index 599be92..8a0ce75 100644 --- a/crates/server/src/router/api/mod.rs +++ b/crates/server/src/router/api/mod.rs @@ -6,6 +6,8 @@ use axum::Json; use axum::Router; use serde::Serialize; +use commune::error::HttpStatusCode; + use crate::services::SharedServices; pub struct Api; @@ -19,13 +21,28 @@ impl Api { #[derive(Debug, Serialize)] pub struct ApiError { message: String, + code: &'static str, #[serde(skip)] status: StatusCode, } impl ApiError { - pub fn new(message: String, status: StatusCode) -> Self { - Self { message, status } + pub fn new(message: String, code: &'static str, status: StatusCode) -> Self { + Self { + message, + code, + status, + } + } +} + +impl From for ApiError { + fn from(err: commune::error::Error) -> Self { + Self { + message: err.to_string(), + code: err.error_code(), + status: err.status_code(), + } } } @@ -40,6 +57,7 @@ impl From for ApiError { fn from(err: anyhow::Error) -> Self { Self { message: err.to_string(), + code: "UNKNOWN_ERROR", status: StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/crates/server/src/router/api/v1/user/register.rs b/crates/server/src/router/api/v1/account/create.rs similarity index 93% rename from crates/server/src/router/api/v1/user/register.rs rename to crates/server/src/router/api/v1/account/create.rs index 72dfc13..b1036b9 100644 --- a/crates/server/src/router/api/v1/user/register.rs +++ b/crates/server/src/router/api/v1/account/create.rs @@ -26,7 +26,7 @@ pub async fn handler( response } Err(err) => { - tracing::error!(?err, "Failed to register user"); + tracing::warn!(?err, "Failed to register user"); ApiError::from(err).into_response() } } @@ -43,7 +43,7 @@ impl From for CreateAccountDto { fn from(payload: UserRegisterPayload) -> Self { Self { username: payload.username, - password: payload.password, + password: payload.password.into(), email: payload.email, // FIXME: These should be queried from somewhere session: "test".to_string(), diff --git a/crates/server/src/router/api/v1/user/mod.rs b/crates/server/src/router/api/v1/account/mod.rs similarity index 55% rename from crates/server/src/router/api/v1/user/mod.rs rename to crates/server/src/router/api/v1/account/mod.rs index 3fd2526..22ee352 100644 --- a/crates/server/src/router/api/v1/user/mod.rs +++ b/crates/server/src/router/api/v1/account/mod.rs @@ -1,14 +1,14 @@ -pub mod register; +pub mod create; use axum::routing::post; use axum::Router; use crate::services::SharedServices; -pub struct User; +pub struct Account; -impl User { +impl Account { pub fn routes() -> Router { - Router::new().route("/register", post(register::handler)) + Router::new().route("/", post(create::handler)) } } diff --git a/crates/server/src/router/api/v1/mod.rs b/crates/server/src/router/api/v1/mod.rs index d7529e7..f57c5bc 100644 --- a/crates/server/src/router/api/v1/mod.rs +++ b/crates/server/src/router/api/v1/mod.rs @@ -1,4 +1,4 @@ -pub mod user; +pub mod account; use axum::Router; @@ -8,6 +8,6 @@ pub struct V1; impl V1 { pub fn routes() -> Router { - Router::new().nest("/user", user::User::routes()) + Router::new().nest("/account", account::Account::routes()) } } diff --git a/crates/test/src/server/api/v1/user/register.rs b/crates/test/src/server/api/v1/account/create.rs similarity index 85% rename from crates/test/src/server/api/v1/user/register.rs rename to crates/test/src/server/api/v1/account/create.rs index 897072a..6c7a080 100644 --- a/crates/test/src/server/api/v1/user/register.rs +++ b/crates/test/src/server/api/v1/account/create.rs @@ -1,6 +1,6 @@ use reqwest::StatusCode; -use commune_server::router::api::v1::user::register::{UserRegisterPayload, UserRegisterResponse}; +use commune_server::router::api::v1::account::create::{UserRegisterPayload, UserRegisterResponse}; use crate::tools::http::HttpClient; @@ -13,7 +13,7 @@ async fn register_account_with_success() { email: String::from("donttrythisathome@gmail.com"), }; let response = http_client - .post("/api/v1/user/register") + .post("/api/v1/account") .json(&request_payload) .send() .await; diff --git a/crates/test/src/server/api/v1/account/mod.rs b/crates/test/src/server/api/v1/account/mod.rs new file mode 100644 index 0000000..0f562a4 --- /dev/null +++ b/crates/test/src/server/api/v1/account/mod.rs @@ -0,0 +1 @@ +mod create; diff --git a/crates/test/src/server/api/v1/mod.rs b/crates/test/src/server/api/v1/mod.rs index 0eba110..cfc38a2 100644 --- a/crates/test/src/server/api/v1/mod.rs +++ b/crates/test/src/server/api/v1/mod.rs @@ -1 +1 @@ -mod user; +mod account; diff --git a/crates/test/src/server/api/v1/user/mod.rs b/crates/test/src/server/api/v1/user/mod.rs deleted file mode 100644 index 5518167..0000000 --- a/crates/test/src/server/api/v1/user/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod register;