use std::collections::HashMap;
use rustrails_support::{database, runtime};
use sea_orm::{
ConnectionTrait, DatabaseConnection,
sea_query::{Alias, Expr, Query},
};
use serde_json::Value;
use crate::{base::RecordError, relation::json_to_sea_value};
#[derive(Debug, Clone, PartialEq)]
pub struct Fixture {
pub model_name: String,
pub records: Vec<HashMap<String, Value>>,
}
impl Fixture {
pub fn from_json(model: &str, json: &str) -> Result<Self, serde_json::Error> {
let records = serde_json::from_str(json)?;
Ok(Self::from_value(model, records))
}
pub fn from_value(model: &str, records: Vec<HashMap<String, Value>>) -> Self {
Self {
model_name: model.to_owned(),
records,
}
}
pub fn records(&self) -> &[HashMap<String, Value>] {
&self.records
}
}
pub async fn load_fixtures(
fixtures: &[Fixture],
db: &DatabaseConnection,
) -> Result<(), RecordError> {
for fixture in fixtures {
for record in fixture.records() {
if record.is_empty() {
continue;
}
let mut query = Query::insert();
query.into_table(Alias::new(&fixture.model_name));
let mut columns = Vec::with_capacity(record.len());
let mut values = Vec::with_capacity(record.len());
for (column, value) in record {
columns.push(Alias::new(column));
values.push(json_to_sea_value(value)?);
}
query.columns(columns);
query.values_panic(values.into_iter().map(Expr::val));
db.execute(&query).await?;
}
}
Ok(())
}
pub fn load_fixtures_sync(fixtures: &[Fixture]) -> Result<(), RecordError> {
database::with_db(|db| runtime::block_on(load_fixtures(fixtures, db)))
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use sea_orm::{ConnectionTrait, Schema};
use serde_json::{Value, error::Category, json};
use super::{Fixture, load_fixtures, load_fixtures_sync};
use crate::{
RecordError,
base::test_support::{TestUser, setup_db, test_user},
querying::AsyncQuerying,
};
use rustrails_support::{database, runtime};
fn run_sync_fixture_test(test: impl FnOnce() + Send + 'static) {
std::thread::spawn(move || {
let _rt = runtime::init_runtime();
database::establish("sqlite::memory:")
.expect("sqlite in-memory connection should succeed");
runtime::block_on(async {
let db = database::db();
let schema = Schema::new(db.get_database_backend());
db.execute(&schema.create_table_from_entity(test_user::Entity))
.await
.expect("test_users table should be created");
});
test();
})
.join()
.unwrap();
}
#[tokio::test]
async fn fixture_parses_from_json() {
let fixture = Fixture::from_json(
"test_users",
r#"[{"name":"Alice","email":"alice@example.com"}]"#,
)
.expect("fixture json should parse");
assert_eq!(fixture.model_name, "test_users");
assert_eq!(fixture.records.len(), 1);
assert_eq!(fixture.records[0]["name"], json!("Alice"));
}
#[tokio::test]
async fn fixture_from_json_rejects_invalid_json_syntax() {
let error = Fixture::from_json("test_users", r#"[{"name":"Alice"}"#)
.expect_err("invalid fixture json should fail");
assert!(matches!(error.classify(), Category::Syntax | Category::Eof));
}
#[tokio::test]
async fn fixture_from_json_rejects_object_root() {
let error = Fixture::from_json("test_users", r#"{"name":"Alice"}"#)
.expect_err("object payload should fail");
assert_eq!(error.classify(), Category::Data);
}
#[tokio::test]
async fn fixture_from_json_rejects_non_object_entries() {
let error = Fixture::from_json("test_users", r#"["Alice"]"#)
.expect_err("non-object records should fail");
assert_eq!(error.classify(), Category::Data);
}
#[tokio::test]
async fn records_accessor_returns_loaded_rows() {
let fixture = Fixture::from_value(
"test_users",
vec![HashMap::from([
("name".to_owned(), json!("Bob")),
("email".to_owned(), json!("bob@example.com")),
])],
);
assert_eq!(fixture.records().len(), 1);
assert_eq!(fixture.records()[0]["email"], json!("bob@example.com"));
}
#[tokio::test]
async fn load_fixtures_inserts_records() {
let db = setup_db().await;
let fixtures = [Fixture::from_value(
"test_users",
vec![
HashMap::from([
("name".to_owned(), json!("Alice")),
("email".to_owned(), json!("alice@example.com")),
]),
HashMap::from([
("name".to_owned(), json!("Bob")),
("email".to_owned(), json!("bob@example.com")),
]),
],
)];
load_fixtures(&fixtures, &db)
.await
.expect("fixtures should load");
let users = TestUser::all(&db).await.expect("records should load");
assert_eq!(users.len(), 2);
assert_eq!(users[0].name, "Alice");
assert_eq!(users[1].name, "Bob");
}
#[tokio::test]
async fn load_fixtures_skips_empty_records() {
let db = setup_db().await;
let fixtures = [Fixture::from_value(
"test_users",
vec![
HashMap::new(),
HashMap::from([
("name".to_owned(), json!("Alice")),
("email".to_owned(), json!("alice@example.com")),
]),
],
)];
load_fixtures(&fixtures, &db)
.await
.expect("non-empty fixtures should load");
let users = TestUser::all(&db).await.expect("records should load");
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, "Alice");
}
#[tokio::test]
async fn load_fixtures_rejects_null_values() {
let db = setup_db().await;
let fixtures = [Fixture::from_value(
"test_users",
vec![HashMap::from([
("name".to_owned(), json!("Alice")),
("email".to_owned(), Value::Null),
])],
)];
let error = load_fixtures(&fixtures, &db)
.await
.expect_err("null values should fail");
assert!(matches!(error, RecordError::Invalid(_)));
}
#[tokio::test]
async fn load_fixtures_rejects_non_scalar_values() {
let db = setup_db().await;
let fixtures = [Fixture::from_value(
"test_users",
vec![HashMap::from([
("name".to_owned(), json!("Alice")),
("email".to_owned(), json!({"address": "alice@example.com"})),
])],
)];
let error = load_fixtures(&fixtures, &db)
.await
.expect_err("non-scalar values should fail");
assert!(matches!(error, RecordError::Invalid(_)));
}
#[tokio::test]
async fn load_fixtures_inserts_multiple_fixture_groups() {
let db = setup_db().await;
let fixtures = [
Fixture::from_value(
"test_users",
vec![HashMap::from([
("name".to_owned(), json!("Alice")),
("email".to_owned(), json!("alice@example.com")),
])],
),
Fixture::from_value(
"test_users",
vec![
HashMap::from([
("name".to_owned(), json!("Bob")),
("email".to_owned(), json!("bob@example.com")),
]),
HashMap::from([
("name".to_owned(), json!("Carol")),
("email".to_owned(), json!("carol@example.com")),
]),
],
),
];
load_fixtures(&fixtures, &db)
.await
.expect("fixture groups should load");
let mut names = TestUser::all(&db)
.await
.expect("records should load")
.into_iter()
.map(|user| user.name)
.collect::<Vec<_>>();
names.sort();
assert_eq!(names, vec!["Alice", "Bob", "Carol"]);
}
#[test]
fn load_fixtures_sync_inserts_records() {
run_sync_fixture_test(|| {
let fixtures = [Fixture::from_value(
"test_users",
vec![HashMap::from([
("name".to_owned(), json!("Alice")),
("email".to_owned(), json!("alice@example.com")),
])],
)];
load_fixtures_sync(&fixtures).expect("fixtures should load");
let users = runtime::block_on(async {
let db = database::db();
TestUser::all(&db).await.expect("records should load")
});
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, "Alice");
});
}
}