Add some tests for the syn2mas MasWriter (#3800)
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user