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#[derive(Debug, Clone, PartialEq)]
14pub struct Fixture {
15 pub model_name: String,
17 pub records: Vec<HashMap<String, Value>>,
19}
20
21impl Fixture {
22 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 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 pub fn records(&self) -> &[HashMap<String, Value>] {
38 &self.records
39 }
40}
41
42pub 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
73pub 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}