Add some tests for the syn2mas MasWriter (#3800)

This commit is contained in:
reivilibre
2025-01-22 18:44:25 +00:00
committed by reivilibre
parent dd0299fa40
commit 759750ed01
5 changed files with 212 additions and 1 deletions

4
Cargo.lock generated
View File

@@ -6111,10 +6111,14 @@ dependencies = [
name = "syn2mas"
version = "0.13.0-rc.1"
dependencies = [
"anyhow",
"chrono",
"compact_str",
"futures-util",
"insta",
"mas-storage-pg",
"rand",
"serde",
"sqlx",
"thiserror 2.0.11",
"thiserror-ext",

View File

@@ -23,5 +23,12 @@ rand.workspace = true
uuid = "1.10.0"
ulid = { workspace = true, features = ["uuid"] }
[dev-dependencies]
mas-storage-pg.workspace = true
anyhow.workspace = true
insta.workspace = true
serde.workspace = true
[lints]
workspace = true

View File

@@ -667,5 +667,176 @@ impl<'writer, 'conn> MasUserWriteBuffer<'writer, 'conn> {
#[cfg(test)]
mod test {
// TODO test me
use std::collections::{BTreeMap, BTreeSet};
use chrono::DateTime;
use futures_util::TryStreamExt;
use serde::Serialize;
use sqlx::{Column, PgConnection, PgPool, Row};
use uuid::Uuid;
use crate::{
mas_writer::{MasNewUser, MasNewUserPassword},
LockedMasDatabase, MasWriter,
};
/// A snapshot of a whole database
#[derive(Default, Serialize)]
#[serde(transparent)]
struct DatabaseSnapshot {
tables: BTreeMap<String, TableSnapshot>,
}
#[derive(Serialize)]
#[serde(transparent)]
struct TableSnapshot {
rows: BTreeSet<RowSnapshot>,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
struct RowSnapshot {
columns_to_values: BTreeMap<String, Option<String>>,
}
const SKIPPED_TABLES: &[&str] = &["_sqlx_migrations"];
/// Produces a serialisable snapshot of a database, usable for snapshot testing
///
/// For brevity, empty tables, as well as [`SKIPPED_TABLES`], will not be included in the snapshot.
async fn snapshot_database(conn: &mut PgConnection) -> DatabaseSnapshot {
let mut out = DatabaseSnapshot::default();
let table_names: Vec<String> = sqlx::query_scalar(
"SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema();",
)
.fetch_all(&mut *conn)
.await
.unwrap();
for table_name in table_names {
if SKIPPED_TABLES.contains(&table_name.as_str()) {
continue;
}
let column_names: Vec<String> = sqlx::query_scalar(
"SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = current_schema();"
).bind(&table_name).fetch_all(&mut *conn).await.expect("failed to get column names for table for snapshotting");
let column_name_list = column_names
.iter()
// stringify all the values for simplicity
.map(|column_name| format!("{column_name}::TEXT AS \"{column_name}\""))
.collect::<Vec<_>>()
.join(", ");
let table_rows = sqlx::query(&format!("SELECT {column_name_list} FROM {table_name};"))
.fetch(&mut *conn)
.map_ok(|row| {
let mut columns_to_values = BTreeMap::new();
for (idx, column) in row.columns().iter().enumerate() {
columns_to_values.insert(column.name().to_owned(), row.get(idx));
}
RowSnapshot { columns_to_values }
})
.try_collect::<BTreeSet<RowSnapshot>>()
.await
.expect("failed to fetch rows from table for snapshotting");
if !table_rows.is_empty() {
out.tables
.insert(table_name, TableSnapshot { rows: table_rows });
}
}
out
}
/// Make a snapshot assertion against the database.
macro_rules! assert_db_snapshot {
($db: expr) => {
let db_snapshot = snapshot_database($db).await;
::insta::assert_yaml_snapshot!(db_snapshot);
};
}
/// Runs some code with a `MasWriter`.
///
/// The callback is responsible for `finish`ing the `MasWriter`.
async fn make_mas_writer<'conn>(
pool: &PgPool,
main_conn: &'conn mut PgConnection,
) -> MasWriter<'conn> {
let mut writer_conns = Vec::new();
for _ in 0..2 {
writer_conns.push(
pool.acquire()
.await
.expect("failed to acquire MasWriter writer connection")
.detach(),
);
}
let locked_main_conn = LockedMasDatabase::try_new(main_conn)
.await
.expect("failed to lock MAS database")
.expect_left("MAS database is already locked");
MasWriter::new(locked_main_conn, writer_conns)
.await
.expect("failed to construct MasWriter")
}
/// Tests writing a single user, without a password.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_write_user(pool: PgPool) {
let mut conn = pool.acquire().await.unwrap();
let mut writer = make_mas_writer(&pool, &mut conn).await;
writer
.write_users(vec![MasNewUser {
user_id: Uuid::from_u128(1u128),
username: "alice".to_owned(),
created_at: DateTime::default(),
locked_at: None,
can_request_admin: false,
}])
.await
.expect("failed to write user");
writer.finish().await.expect("failed to finish MasWriter");
assert_db_snapshot!(&mut conn);
}
/// Tests writing a single user, with a password.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_write_user_with_password(pool: PgPool) {
const USER_ID: Uuid = Uuid::from_u128(1u128);
let mut conn = pool.acquire().await.unwrap();
let mut writer = make_mas_writer(&pool, &mut conn).await;
writer
.write_users(vec![MasNewUser {
user_id: USER_ID,
username: "alice".to_owned(),
created_at: DateTime::default(),
locked_at: None,
can_request_admin: false,
}])
.await
.expect("failed to write user");
writer
.write_passwords(vec![MasNewUserPassword {
user_password_id: Uuid::from_u128(42u128),
user_id: USER_ID,
hashed_password: "$bcrypt$aaaaaaaaaaa".to_owned(),
created_at: DateTime::default(),
}])
.await
.expect("failed to write password");
writer.finish().await.expect("failed to finish MasWriter");
assert_db_snapshot!(&mut conn);
}
}

View File

@@ -0,0 +1,11 @@
---
source: crates/syn2mas/src/mas_writer/mod.rs
expression: db_snapshot
---
users:
- can_request_admin: "false"
created_at: "1970-01-01 00:00:00+00"
locked_at: ~
primary_user_email_id: ~
user_id: 00000000-0000-0000-0000-000000000001
username: alice

View File

@@ -0,0 +1,18 @@
---
source: crates/syn2mas/src/mas_writer/mod.rs
expression: db_snapshot
---
user_passwords:
- created_at: "1970-01-01 00:00:00+00"
hashed_password: $bcrypt$aaaaaaaaaaa
upgraded_from_id: ~
user_id: 00000000-0000-0000-0000-000000000001
user_password_id: 00000000-0000-0000-0000-00000000002a
version: "1"
users:
- can_request_admin: "false"
created_at: "1970-01-01 00:00:00+00"
locked_at: ~
primary_user_email_id: ~
user_id: 00000000-0000-0000-0000-000000000001
username: alice