Skip to main content

rustrails_record/
fixtures.rs

1use std::collections::HashMap;
2
3use rustrails_support::{database, runtime};
4use sea_orm::{
5    ConnectionTrait, DatabaseConnection,
6    sea_query::{Alias, Expr, Query},
7};
8use serde_json::Value;
9
10use crate::{base::RecordError, relation::json_to_sea_value};
11
12/// Fixture data for tests and seeds.
13#[derive(Debug, Clone, PartialEq)]
14pub struct Fixture {
15    /// The target table or model name.
16    pub model_name: String,
17    /// The records to insert.
18    pub records: Vec<HashMap<String, Value>>,
19}
20
21impl Fixture {
22    /// Parses fixture records from a JSON array of objects.
23    pub fn from_json(model: &str, json: &str) -> Result<Self, serde_json::Error> {
24        let records = serde_json::from_str(json)?;
25        Ok(Self::from_value(model, records))
26    }
27
28    /// Builds a fixture from already-parsed records.
29    pub fn from_value(model: &str, records: Vec<HashMap<String, Value>>) -> Self {
30        Self {
31            model_name: model.to_owned(),
32            records,
33        }
34    }
35
36    /// Returns the contained records.
37    pub fn records(&self) -> &[HashMap<String, Value>] {
38        &self.records
39    }
40}
41
42/// Loads fixtures into the database.
43pub async fn load_fixtures(
44    fixtures: &[Fixture],
45    db: &DatabaseConnection,
46) -> Result<(), RecordError> {
47    for fixture in fixtures {
48        for record in fixture.records() {
49            if record.is_empty() {
50                continue;
51            }
52
53            let mut query = Query::insert();
54            query.into_table(Alias::new(&fixture.model_name));
55
56            let mut columns = Vec::with_capacity(record.len());
57            let mut values = Vec::with_capacity(record.len());
58            for (column, value) in record {
59                columns.push(Alias::new(column));
60                values.push(json_to_sea_value(value)?);
61            }
62
63            query.columns(columns);
64            query.values_panic(values.into_iter().map(Expr::val));
65
66            db.execute(&query).await?;
67        }
68    }
69
70    Ok(())
71}
72
73/// Synchronous wrapper for [`load_fixtures`].
74pub fn load_fixtures_sync(fixtures: &[Fixture]) -> Result<(), RecordError> {
75    database::with_db(|db| runtime::block_on(load_fixtures(fixtures, db)))
76}
77
78#[cfg(test)]
79mod tests {
80    use std::collections::HashMap;
81
82    use sea_orm::{ConnectionTrait, Schema};
83    use serde_json::{Value, error::Category, json};
84
85    use super::{Fixture, load_fixtures, load_fixtures_sync};
86    use crate::{
87        RecordError,
88        base::test_support::{TestUser, setup_db, test_user},
89        querying::AsyncQuerying,
90    };
91    use rustrails_support::{database, runtime};
92
93    fn run_sync_fixture_test(test: impl FnOnce() + Send + 'static) {
94        std::thread::spawn(move || {
95            let _rt = runtime::init_runtime();
96            database::establish("sqlite::memory:")
97                .expect("sqlite in-memory connection should succeed");
98            runtime::block_on(async {
99                let db = database::db();
100                let schema = Schema::new(db.get_database_backend());
101                db.execute(&schema.create_table_from_entity(test_user::Entity))
102                    .await
103                    .expect("test_users table should be created");
104            });
105            test();
106        })
107        .join()
108        .unwrap();
109    }
110
111    #[tokio::test]
112    async fn fixture_parses_from_json() {
113        let fixture = Fixture::from_json(
114            "test_users",
115            r#"[{"name":"Alice","email":"alice@example.com"}]"#,
116        )
117        .expect("fixture json should parse");
118
119        assert_eq!(fixture.model_name, "test_users");
120        assert_eq!(fixture.records.len(), 1);
121        assert_eq!(fixture.records[0]["name"], json!("Alice"));
122    }
123
124    #[tokio::test]
125    async fn fixture_from_json_rejects_invalid_json_syntax() {
126        let error = Fixture::from_json("test_users", r#"[{"name":"Alice"}"#)
127            .expect_err("invalid fixture json should fail");
128
129        assert!(matches!(error.classify(), Category::Syntax | Category::Eof));
130    }
131
132    #[tokio::test]
133    async fn fixture_from_json_rejects_object_root() {
134        let error = Fixture::from_json("test_users", r#"{"name":"Alice"}"#)
135            .expect_err("object payload should fail");
136
137        assert_eq!(error.classify(), Category::Data);
138    }
139
140    #[tokio::test]
141    async fn fixture_from_json_rejects_non_object_entries() {
142        let error = Fixture::from_json("test_users", r#"["Alice"]"#)
143            .expect_err("non-object records should fail");
144
145        assert_eq!(error.classify(), Category::Data);
146    }
147
148    #[tokio::test]
149    async fn records_accessor_returns_loaded_rows() {
150        let fixture = Fixture::from_value(
151            "test_users",
152            vec![HashMap::from([
153                ("name".to_owned(), json!("Bob")),
154                ("email".to_owned(), json!("bob@example.com")),
155            ])],
156        );
157
158        assert_eq!(fixture.records().len(), 1);
159        assert_eq!(fixture.records()[0]["email"], json!("bob@example.com"));
160    }
161
162    #[tokio::test]
163    async fn load_fixtures_inserts_records() {
164        let db = setup_db().await;
165        let fixtures = [Fixture::from_value(
166            "test_users",
167            vec![
168                HashMap::from([
169                    ("name".to_owned(), json!("Alice")),
170                    ("email".to_owned(), json!("alice@example.com")),
171                ]),
172                HashMap::from([
173                    ("name".to_owned(), json!("Bob")),
174                    ("email".to_owned(), json!("bob@example.com")),
175                ]),
176            ],
177        )];
178
179        load_fixtures(&fixtures, &db)
180            .await
181            .expect("fixtures should load");
182
183        let users = TestUser::all(&db).await.expect("records should load");
184        assert_eq!(users.len(), 2);
185        assert_eq!(users[0].name, "Alice");
186        assert_eq!(users[1].name, "Bob");
187    }
188
189    #[tokio::test]
190    async fn load_fixtures_skips_empty_records() {
191        let db = setup_db().await;
192        let fixtures = [Fixture::from_value(
193            "test_users",
194            vec![
195                HashMap::new(),
196                HashMap::from([
197                    ("name".to_owned(), json!("Alice")),
198                    ("email".to_owned(), json!("alice@example.com")),
199                ]),
200            ],
201        )];
202
203        load_fixtures(&fixtures, &db)
204            .await
205            .expect("non-empty fixtures should load");
206
207        let users = TestUser::all(&db).await.expect("records should load");
208        assert_eq!(users.len(), 1);
209        assert_eq!(users[0].name, "Alice");
210    }
211
212    #[tokio::test]
213    async fn load_fixtures_rejects_null_values() {
214        let db = setup_db().await;
215        let fixtures = [Fixture::from_value(
216            "test_users",
217            vec![HashMap::from([
218                ("name".to_owned(), json!("Alice")),
219                ("email".to_owned(), Value::Null),
220            ])],
221        )];
222
223        let error = load_fixtures(&fixtures, &db)
224            .await
225            .expect_err("null values should fail");
226
227        assert!(matches!(error, RecordError::Invalid(_)));
228    }
229
230    #[tokio::test]
231    async fn load_fixtures_rejects_non_scalar_values() {
232        let db = setup_db().await;
233        let fixtures = [Fixture::from_value(
234            "test_users",
235            vec![HashMap::from([
236                ("name".to_owned(), json!("Alice")),
237                ("email".to_owned(), json!({"address": "alice@example.com"})),
238            ])],
239        )];
240
241        let error = load_fixtures(&fixtures, &db)
242            .await
243            .expect_err("non-scalar values should fail");
244
245        assert!(matches!(error, RecordError::Invalid(_)));
246    }
247
248    #[tokio::test]
249    async fn load_fixtures_inserts_multiple_fixture_groups() {
250        let db = setup_db().await;
251        let fixtures = [
252            Fixture::from_value(
253                "test_users",
254                vec![HashMap::from([
255                    ("name".to_owned(), json!("Alice")),
256                    ("email".to_owned(), json!("alice@example.com")),
257                ])],
258            ),
259            Fixture::from_value(
260                "test_users",
261                vec![
262                    HashMap::from([
263                        ("name".to_owned(), json!("Bob")),
264                        ("email".to_owned(), json!("bob@example.com")),
265                    ]),
266                    HashMap::from([
267                        ("name".to_owned(), json!("Carol")),
268                        ("email".to_owned(), json!("carol@example.com")),
269                    ]),
270                ],
271            ),
272        ];
273
274        load_fixtures(&fixtures, &db)
275            .await
276            .expect("fixture groups should load");
277
278        let mut names = TestUser::all(&db)
279            .await
280            .expect("records should load")
281            .into_iter()
282            .map(|user| user.name)
283            .collect::<Vec<_>>();
284        names.sort();
285
286        assert_eq!(names, vec!["Alice", "Bob", "Carol"]);
287    }
288
289    #[test]
290    fn load_fixtures_sync_inserts_records() {
291        run_sync_fixture_test(|| {
292            let fixtures = [Fixture::from_value(
293                "test_users",
294                vec![HashMap::from([
295                    ("name".to_owned(), json!("Alice")),
296                    ("email".to_owned(), json!("alice@example.com")),
297                ])],
298            )];
299
300            load_fixtures_sync(&fixtures).expect("fixtures should load");
301
302            let users = runtime::block_on(async {
303                let db = database::db();
304                TestUser::all(&db).await.expect("records should load")
305            });
306            assert_eq!(users.len(), 1);
307            assert_eq!(users[0].name, "Alice");
308        });
309    }
310}