Skip to content

Commit

Permalink
test: make TestApp generic over core_client
Browse files Browse the repository at this point in the history
  • Loading branch information
c-git committed Dec 15, 2024
1 parent a9ad56c commit 2d72546
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 84 deletions.
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/chat-app-server/tests/api/change_password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async fn new_password_fields_must_match() {
.core_client
.change_password(
&ChangePasswordReqArgs {
current_password: app.test_user.password.into(),
current_password: app.test_user.password.clone().into(),
new_password: new_password.clone().into(),
new_password_check: another_new_password.into(),
},
Expand Down
2 changes: 1 addition & 1 deletion crates/chat-app-server/tests/api/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async fn sent_messages_received() {
.await
.expect("failed to receive on rx")
.expect("connection result was not ok");
let author: Username = app.test_user.username.try_into().unwrap();
let author: Username = app.test_user.username.clone().try_into().unwrap();
let expected_im = ChatMsg::IM(ChatIM {
author: author.clone(),
timestamp: Timestamp::now(),
Expand Down
94 changes: 90 additions & 4 deletions crates/chat-app-server/tests/api/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
use chat_app_server::startup::{start_servers, CustomConfiguration};
use std::mem::forget;
use std::{
mem::forget,
ops::{Deref, DerefMut},
};
use tracked_cancellations::TrackedCancellationToken;
use wykies_client_core::LoginOutcome;
use wykies_server::{db_types::DbPool, ApiServerBuilder, Configuration};
use wykies_server_test_helper::{
build_test_app, spawn_app_without_host_branch_stored_before_migration, store_host_branch,
build_test_app, port_to_test_address, spawn_app_without_host_branch_stored_before_migration,
store_host_branch, TestUser,
};
use wykies_shared::const_config::path::PATH_WS_TOKEN_CHAT;

pub use wykies_server_test_helper::{no_cb, wait_for_message};

#[derive(Debug)]
pub struct TestApp(wykies_server_test_helper::TestApp<wykies_client_core::Client>);

impl Deref for TestApp {
type Target = wykies_server_test_helper::TestApp<wykies_client_core::Client>;

pub use wykies_server_test_helper::{no_cb, wait_for_message, TestApp};
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl DerefMut for TestApp {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

pub async fn spawn_app() -> TestApp {
let result = spawn_app_without_host_branch_stored().await;
Expand All @@ -19,7 +42,14 @@ pub async fn spawn_app_without_host_branch_stored() -> TestApp {
spawn_app_without_host_branch_stored_before_migration::<CustomConfiguration>().await;
do_migrations(&connection_pool).await;
let application_port = start_server_in_background(&configuration).await;
build_test_app(configuration, application_port).await
TestApp(
build_test_app(
configuration,
port_to_test_address(application_port),
wykies_client_core::Client::new,
)
.await,
)
}

async fn do_migrations(connection_pool: &DbPool) {
Expand All @@ -45,3 +75,59 @@ async fn start_server_in_background(configuration: &Configuration<CustomConfigur
// Leak the JoinSet so the server doesn't get shutdown
application_port
}

impl TestApp {
/// Creates a clone of [`Self`] with an admin user and separate api_client
#[tracing::instrument]
pub async fn create_admin_user(&self) -> Self {
let admin_user = TestUser::generate("admin");
admin_user.store(&self.db_pool, true).await;
Self(
wykies_server_test_helper::TestApp::<wykies_client_core::Client> {
address: self.address.clone(),
db_pool: self.db_pool.clone(),
test_user: admin_user,
core_client: wykies_client_core::Client::new(self.address.clone()),
login_attempt_limit: self.login_attempt_limit,
host_branch_pair: self.host_branch_pair.clone(),
},
)
}

#[tracing::instrument]
pub async fn is_logged_in(&self) -> bool {
// Also tests if able to establish a websocket connection but this was the simplest alternative that didn't need any permissions
self.core_client
.ws_connect(PATH_WS_TOKEN_CHAT, no_cb)
.await
.expect("failed to receive on rx")
.is_ok()
}

pub async fn login(&self) -> anyhow::Result<LoginOutcome> {
self.core_client
.login(self.test_user.login_args(), no_cb)
.await
.unwrap()
}

/// Logs in the user and panics if the login is not successful
pub async fn login_assert(&self) {
assert!(self
.core_client
.login(self.test_user.login_args(), no_cb)
.await
.expect("failed to receive on rx")
.expect("failed to extract login outcome")
.is_any_success());
}

/// Logs out the user and panics on errors
pub async fn logout_assert(&self) {
self.core_client
.logout(no_cb)
.await
.expect("failed to receive on rx")
.expect("login result was not ok");
}
}
2 changes: 0 additions & 2 deletions crates/wykies-server-test-helper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ rand = { workspace = true, features = ["std_rng"] }
serde.workspace = true
sqlx = { workspace = true, features = ["runtime-tokio", "macros", "mysql", "chrono", "migrate"] }
tokio.workspace = true
tracing.workspace = true
uuid = { workspace = true, features = ["v4", "serde"] }
wykies-client-core = { workspace = true, features = ["expose_test"] }
wykies-server.workspace = true
wykies-shared = { workspace = true, features = ["server_only"] }
wykies-time = { workspace = true, features = ["mysql"] }
92 changes: 18 additions & 74 deletions crates/wykies-server-test-helper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ use std::ops::Deref;
use std::sync::LazyLock;
use std::time::{Duration, Instant};
use uuid::Uuid;
use wykies_client_core::LoginOutcome;
use wykies_server::Configuration;
use wykies_server::{
db_types::{DbConnection, DbPool},
db_utils::validate_one_row_affected,
get_configuration, get_db_connection_pool, DatabaseSettings,
};
use wykies_shared::{
const_config::path::PATH_WS_TOKEN_CHAT,
host_branch::HostBranchPair,
id::DbId,
req_args::LoginReqArgs,
Expand Down Expand Up @@ -46,88 +44,27 @@ pub static TRACING: LazyLock<String> = LazyLock::new(|| {
}
});

pub struct TestApp {
pub struct TestApp<C> {
pub address: String,
pub port: u16,
pub db_pool: DbPool,
pub test_user: TestUser,
pub core_client: wykies_client_core::Client,
pub core_client: C,
pub login_attempt_limit: u8,
pub host_branch_pair: HostBranchPair,
}

impl Debug for TestApp {
impl<C> Debug for TestApp<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestApp")
.field("address", &self.address)
.field("port", &self.port)
.field("test_user", &self.test_user)
.finish()
}
}

impl TestApp {
/// Creates a clone of [`Self`] with an admin user and separate api_client
#[tracing::instrument]
pub async fn create_admin_user(&self) -> Self {
let admin_user = TestUser::generate("admin");
admin_user.store(&self.db_pool, true).await;
Self {
address: self.address.clone(),
port: self.port,
db_pool: self.db_pool.clone(),
test_user: admin_user,
core_client: build_core_client(self.address.clone()),
login_attempt_limit: self.login_attempt_limit,
host_branch_pair: self.host_branch_pair.clone(),
}
}

#[tracing::instrument]
pub async fn is_logged_in(&self) -> bool {
// Also tests if able to establish a websocket connection but this was the simplest alternative that didn't need any permissions
self.core_client
.ws_connect(PATH_WS_TOKEN_CHAT, no_cb)
.await
.expect("failed to receive on rx")
.is_ok()
}

pub async fn login(&self) -> anyhow::Result<LoginOutcome> {
self.core_client
.login(self.test_user.login_args(), no_cb)
.await
.unwrap()
}

/// Logs in the user and panics if the login is not successful
pub async fn login_assert(&self) {
assert!(self
.core_client
.login(self.test_user.login_args(), no_cb)
.await
.expect("failed to receive on rx")
.expect("failed to extract login outcome")
.is_any_success());
}

/// Logs out the user and panics on errors
pub async fn logout_assert(&self) {
self.core_client
.logout(no_cb)
.await
.expect("failed to receive on rx")
.expect("login result was not ok");
}
}

/// Empty function for use when a call back isn't needed
pub fn no_cb() {}

fn build_core_client(server_address: String) -> wykies_client_core::Client {
wykies_client_core::Client::new(server_address)
}

pub async fn spawn_app_without_host_branch_stored_before_migration<T>() -> (Configuration<T>, DbPool)
where
T: Clone + DeserializeOwned,
Expand All @@ -138,22 +75,29 @@ where
(configuration, connection_pool)
}

pub async fn build_test_app<T>(configuration: Configuration<T>, application_port: u16) -> TestApp
pub fn port_to_test_address(application_port: u16) -> String {
format!("http://localhost:{application_port}")
}

pub async fn build_test_app<T, C, F>(
configuration: Configuration<T>,
address: String,
build_client: F,
) -> TestApp<C>
where
T: Clone + DeserializeOwned,
F: FnOnce(String) -> C,
{
let login_attempt_limit = configuration.user_auth.login_attempt_limit;
let db_pool = get_db_connection_pool(&configuration.database);
let host_branch_pair = HostBranchPair {
host_id: "127.0.0.1".to_string().try_into().unwrap(),
branch_id: get_seed_branch_from_db(&db_pool).await,
};
let address = format!("http://localhost:{}", application_port);
let core_client = build_core_client(address.clone());
let core_client = build_client(address.clone());

let test_app = TestApp {
address,
port: application_port,
db_pool,
test_user: TestUser::generate("normal"),
core_client,
Expand Down Expand Up @@ -234,7 +178,7 @@ impl TestUser {
LoginReqArgs::new(self.username.clone(), self.password.clone().into())
}

pub async fn disable_in_db(&self, app: &TestApp) {
pub async fn disable_in_db<C>(&self, app: &TestApp<C>) {
let sql_result = sqlx::query!(
"UPDATE `user` SET `Enabled` = '0' WHERE `user`.`UserName` = ?;",
self.username,
Expand All @@ -245,7 +189,7 @@ impl TestUser {
validate_one_row_affected(&sql_result).expect("failed to set user to disabled");
}

pub async fn set_locked_out_in_db(&self, app: &TestApp, value: bool) {
pub async fn set_locked_out_in_db<C>(&self, app: &TestApp<C>, value: bool) {
let value = if value { 1 } else { 0 };
let sql_result = sqlx::query!(
"UPDATE `user` SET `LockedOut` = ? WHERE `user`.`UserName` = ?;",
Expand All @@ -258,7 +202,7 @@ impl TestUser {
validate_one_row_affected(&sql_result).expect("failed to set user to disabled");
}

async fn store(&self, pool: &DbPool, is_admin: bool) {
pub async fn store(&self, pool: &DbPool, is_admin: bool) {
let salt = SaltString::generate(&mut rand::thread_rng());
// Match production parameters
let password_hash = wykies_server::authentication::argon2_settings()
Expand Down Expand Up @@ -331,7 +275,7 @@ pub async fn wait_for_message(
bail!("Timed out after {MSG_WAIT_TIMEOUT:?}")
}

pub async fn store_host_branch(test_app: &TestApp) {
pub async fn store_host_branch<C>(test_app: &TestApp<C>) {
let sql_result = sqlx::query!(
"INSERT INTO `hostbranch`
(`hostname`, `AssignedBranch`)
Expand Down

0 comments on commit 2d72546

Please sign in to comment.