Skip to main content

mockforge_federation/
database.rs

1//! Database persistence for federations
2//!
3//! Provides methods to store and retrieve federation configurations from the database.
4
5use crate::federation::Federation;
6use crate::service::{ServiceBoundary, ServiceRealityLevel};
7use anyhow::{Context, Result};
8use chrono::{DateTime, Utc};
9use serde_json;
10use sqlx::{sqlite::SqlitePool, Row};
11use tracing::info;
12use uuid::Uuid;
13
14/// Database layer for federation persistence
15pub struct FederationDatabase {
16    pool: SqlitePool,
17}
18
19impl FederationDatabase {
20    /// Create a new federation database instance
21    ///
22    /// # Errors
23    ///
24    /// This function does not currently return errors but is async for future migration support.
25    #[allow(clippy::unused_async)]
26    pub async fn new(pool: SqlitePool) -> Result<Self> {
27        // Run migrations
28        // Note: In a real implementation, you'd use sqlx::migrate! macro
29        // For now, we'll run migrations manually or via a migration runner
30        // sqlx::migrate!("./migrations")
31        //     .run(&pool)
32        //     .await
33        //     .context("Failed to run federation migrations")?;
34
35        Ok(Self { pool })
36    }
37
38    /// Run migrations manually
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if the migration SQL fails to execute.
43    pub async fn run_migrations(&self) -> Result<()> {
44        let migration_sql = include_str!("../migrations/001_federation.sql");
45
46        sqlx::query(migration_sql)
47            .execute(&self.pool)
48            .await
49            .context("Failed to run federation migrations")?;
50
51        info!("Federation database migrations completed");
52        Ok(())
53    }
54
55    /// Create a new federation
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the database insert fails.
60    pub async fn create_federation(&self, federation: &Federation) -> Result<()> {
61        let id_str = federation.id.to_string();
62        let org_id_str = federation.org_id.to_string();
63        let created_at = federation.created_at.timestamp();
64        let updated_at = federation.updated_at.timestamp();
65
66        sqlx::query(
67            r"
68            INSERT INTO federations (id, name, org_id, description, created_at, updated_at)
69            VALUES (?1, ?2, ?3, ?4, ?5, ?6)
70            ",
71        )
72        .bind(&id_str)
73        .bind(&federation.name)
74        .bind(&org_id_str)
75        .bind(&federation.description)
76        .bind(created_at)
77        .bind(updated_at)
78        .execute(&self.pool)
79        .await
80        .context("Failed to create federation")?;
81
82        // Insert services
83        for service in &federation.services {
84            self.create_federation_service(&id_str, service).await?;
85        }
86
87        info!(
88            federation_id = %federation.id,
89            federation_name = %federation.name,
90            "Created federation"
91        );
92
93        Ok(())
94    }
95
96    /// Create a federation service
97    async fn create_federation_service(
98        &self,
99        federation_id: &str,
100        service: &ServiceBoundary,
101    ) -> Result<()> {
102        let service_id = Uuid::new_v4().to_string();
103        let workspace_id_str = service.workspace_id.to_string();
104        let config_json =
105            serde_json::to_string(&service.config).context("Failed to serialize service config")?;
106        let dependencies_json = serde_json::to_string(&service.dependencies)
107            .context("Failed to serialize dependencies")?;
108        let created_at = Utc::now().timestamp();
109
110        sqlx::query(
111            r"
112            INSERT INTO federation_services
113            (id, federation_id, service_name, workspace_id, base_path, reality_level, config, dependencies, created_at)
114            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
115            ",
116        )
117        .bind(&service_id)
118        .bind(federation_id)
119        .bind(&service.name)
120        .bind(&workspace_id_str)
121        .bind(&service.base_path)
122        .bind(service.reality_level.as_str())
123        .bind(&config_json)
124        .bind(&dependencies_json)
125        .bind(created_at)
126        .execute(&self.pool)
127        .await
128        .context("Failed to create federation service")?;
129
130        Ok(())
131    }
132
133    /// Get a federation by ID
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if the database query fails or data parsing fails.
138    pub async fn get_federation(&self, federation_id: &Uuid) -> Result<Option<Federation>> {
139        let id_str = federation_id.to_string();
140
141        let row = sqlx::query(
142            r"
143            SELECT id, name, org_id, description, created_at, updated_at
144            FROM federations
145            WHERE id = ?1
146            ",
147        )
148        .bind(&id_str)
149        .fetch_optional(&self.pool)
150        .await
151        .context("Failed to query federation")?;
152
153        if let Some(row) = row {
154            let id = Uuid::parse_str(row.get::<String, _>(0).as_str())
155                .context("Invalid federation ID")?;
156            let name: String = row.get(1);
157            let org_id =
158                Uuid::parse_str(row.get::<String, _>(2).as_str()).context("Invalid org ID")?;
159            let description: String = row.get(3);
160            let created_at = DateTime::from_timestamp(row.get::<i64, _>(4), 0)
161                .unwrap_or_else(Utc::now)
162                .with_timezone(&Utc);
163            let updated_at = DateTime::from_timestamp(row.get::<i64, _>(5), 0)
164                .unwrap_or_else(Utc::now)
165                .with_timezone(&Utc);
166
167            // Load services
168            let services = self.get_federation_services(&id_str).await?;
169
170            Ok(Some(Federation {
171                id,
172                name,
173                description,
174                org_id,
175                services,
176                created_at,
177                updated_at,
178            }))
179        } else {
180            Ok(None)
181        }
182    }
183
184    /// Get all services for a federation
185    async fn get_federation_services(&self, federation_id: &str) -> Result<Vec<ServiceBoundary>> {
186        let rows = sqlx::query(
187            r"
188            SELECT service_name, workspace_id, base_path, reality_level, config, dependencies
189            FROM federation_services
190            WHERE federation_id = ?1
191            ORDER BY base_path
192            ",
193        )
194        .bind(federation_id)
195        .fetch_all(&self.pool)
196        .await
197        .context("Failed to query federation services")?;
198
199        let mut services = Vec::new();
200
201        for row in rows {
202            let name: String = row.get(0);
203            let workspace_id = Uuid::parse_str(row.get::<String, _>(1).as_str())
204                .context("Invalid workspace ID")?;
205            let base_path: String = row.get(2);
206            let reality_level_str: String = row.get(3);
207            let config_json: String = row.get(4);
208            let dependencies_json: String = row.get(5);
209
210            let reality_level = ServiceRealityLevel::from_str(&reality_level_str)
211                .ok_or_else(|| anyhow::anyhow!("Invalid reality level: {reality_level_str}"))?;
212
213            let config: std::collections::HashMap<String, serde_json::Value> =
214                serde_json::from_str(&config_json).context("Failed to parse service config")?;
215            let dependencies: Vec<String> =
216                serde_json::from_str(&dependencies_json).context("Failed to parse dependencies")?;
217
218            let mut service = ServiceBoundary::new(name, workspace_id, base_path, reality_level);
219            service.config = config;
220            service.dependencies = dependencies;
221
222            services.push(service);
223        }
224
225        Ok(services)
226    }
227
228    /// List all federations for an organization
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if the database query fails or data parsing fails.
233    pub async fn list_federations(&self, org_id: &Uuid) -> Result<Vec<Federation>> {
234        let org_id_str = org_id.to_string();
235
236        let rows = sqlx::query(
237            r"
238            SELECT id, name, org_id, description, created_at, updated_at
239            FROM federations
240            WHERE org_id = ?1
241            ORDER BY created_at DESC
242            ",
243        )
244        .bind(&org_id_str)
245        .fetch_all(&self.pool)
246        .await
247        .context("Failed to query federations")?;
248
249        let mut federations = Vec::new();
250
251        for row in rows {
252            let id = Uuid::parse_str(row.get::<String, _>(0).as_str())
253                .context("Invalid federation ID")?;
254            let name: String = row.get(1);
255            let org_id =
256                Uuid::parse_str(row.get::<String, _>(2).as_str()).context("Invalid org ID")?;
257            let description: String = row.get(3);
258            let created_at = DateTime::from_timestamp(row.get::<i64, _>(4), 0)
259                .unwrap_or_else(Utc::now)
260                .with_timezone(&Utc);
261            let updated_at = DateTime::from_timestamp(row.get::<i64, _>(5), 0)
262                .unwrap_or_else(Utc::now)
263                .with_timezone(&Utc);
264
265            let id_str = id.to_string();
266            let services = self.get_federation_services(&id_str).await?;
267
268            federations.push(Federation {
269                id,
270                name,
271                description,
272                org_id,
273                services,
274                created_at,
275                updated_at,
276            });
277        }
278
279        Ok(federations)
280    }
281
282    /// Update a federation
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if the database update fails.
287    pub async fn update_federation(&self, federation: &Federation) -> Result<()> {
288        let id_str = federation.id.to_string();
289        let updated_at = Utc::now().timestamp();
290
291        sqlx::query(
292            r"
293            UPDATE federations
294            SET name = ?1, description = ?2, updated_at = ?3
295            WHERE id = ?4
296            ",
297        )
298        .bind(&federation.name)
299        .bind(&federation.description)
300        .bind(updated_at)
301        .bind(&id_str)
302        .execute(&self.pool)
303        .await
304        .context("Failed to update federation")?;
305
306        // Delete existing services and recreate
307        sqlx::query("DELETE FROM federation_services WHERE federation_id = ?1")
308            .bind(&id_str)
309            .execute(&self.pool)
310            .await
311            .context("Failed to delete existing services")?;
312
313        // Recreate services
314        for service in &federation.services {
315            self.create_federation_service(&id_str, service).await?;
316        }
317
318        info!(
319            federation_id = %federation.id,
320            "Updated federation"
321        );
322
323        Ok(())
324    }
325
326    /// Delete a federation
327    ///
328    /// # Errors
329    ///
330    /// Returns an error if the database delete fails.
331    pub async fn delete_federation(&self, federation_id: &Uuid) -> Result<()> {
332        let id_str = federation_id.to_string();
333
334        sqlx::query("DELETE FROM federations WHERE id = ?1")
335            .bind(&id_str)
336            .execute(&self.pool)
337            .await
338            .context("Failed to delete federation")?;
339
340        info!(
341            federation_id = %federation_id,
342            "Deleted federation"
343        );
344
345        Ok(())
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::federation::Federation;
353    use crate::service::{ServiceBoundary, ServiceRealityLevel};
354    use tempfile::TempDir;
355
356    async fn create_test_db() -> (FederationDatabase, TempDir) {
357        let temp_dir = TempDir::new().unwrap();
358        let db_path = temp_dir.path().join("test.db");
359        let db_url = format!("sqlite://{}?mode=rwc", db_path.display());
360
361        let pool = SqlitePool::connect(&db_url).await.unwrap();
362        let db = FederationDatabase::new(pool).await.unwrap();
363        db.run_migrations().await.unwrap();
364
365        (db, temp_dir)
366    }
367
368    fn create_test_federation() -> Federation {
369        let org_id = Uuid::new_v4();
370        let workspace_id1 = Uuid::new_v4();
371        let workspace_id2 = Uuid::new_v4();
372
373        let mut service1 = ServiceBoundary::new(
374            "auth".to_string(),
375            workspace_id1,
376            "/auth".to_string(),
377            ServiceRealityLevel::Real,
378        );
379        service1.config.insert("timeout".to_string(), serde_json::json!(5000));
380        service1.dependencies.push("database".to_string());
381
382        let service2 = ServiceBoundary::new(
383            "payments".to_string(),
384            workspace_id2,
385            "/payments".to_string(),
386            ServiceRealityLevel::MockV3,
387        );
388
389        Federation {
390            id: Uuid::new_v4(),
391            name: "test-federation".to_string(),
392            description: "Test federation for unit tests".to_string(),
393            org_id,
394            services: vec![service1, service2],
395            created_at: Utc::now(),
396            updated_at: Utc::now(),
397        }
398    }
399
400    #[tokio::test]
401    async fn test_new_database() {
402        let temp_dir = TempDir::new().unwrap();
403        let db_path = temp_dir.path().join("test.db");
404        let db_url = format!("sqlite://{}?mode=rwc", db_path.display());
405
406        let pool = SqlitePool::connect(&db_url).await.unwrap();
407        let result = FederationDatabase::new(pool).await;
408
409        assert!(result.is_ok());
410    }
411
412    #[tokio::test]
413    async fn test_run_migrations() {
414        let (db, _temp_dir) = create_test_db().await;
415
416        // Verify tables were created by trying to query them
417        let result = sqlx::query("SELECT COUNT(*) FROM federations").fetch_one(&db.pool).await;
418
419        assert!(result.is_ok());
420    }
421
422    #[tokio::test]
423    async fn test_create_federation() {
424        let (db, _temp_dir) = create_test_db().await;
425        let federation = create_test_federation();
426
427        let result = db.create_federation(&federation).await;
428        assert!(result.is_ok());
429
430        // Verify it was inserted
431        let row = sqlx::query("SELECT COUNT(*) FROM federations WHERE id = ?1")
432            .bind(federation.id.to_string())
433            .fetch_one(&db.pool)
434            .await
435            .unwrap();
436
437        let count: i64 = row.get(0);
438        assert_eq!(count, 1);
439    }
440
441    #[tokio::test]
442    async fn test_create_federation_with_services() {
443        let (db, _temp_dir) = create_test_db().await;
444        let federation = create_test_federation();
445        let federation_id = federation.id;
446
447        db.create_federation(&federation).await.unwrap();
448
449        // Verify services were created
450        let rows = sqlx::query("SELECT COUNT(*) FROM federation_services WHERE federation_id = ?1")
451            .bind(federation_id.to_string())
452            .fetch_one(&db.pool)
453            .await
454            .unwrap();
455
456        let count: i64 = rows.get(0);
457        assert_eq!(count, 2);
458    }
459
460    #[tokio::test]
461    async fn test_get_federation() {
462        let (db, _temp_dir) = create_test_db().await;
463        let original = create_test_federation();
464        let federation_id = original.id;
465
466        db.create_federation(&original).await.unwrap();
467
468        let result = db.get_federation(&federation_id).await.unwrap();
469        assert!(result.is_some());
470
471        let retrieved = result.unwrap();
472        assert_eq!(retrieved.id, original.id);
473        assert_eq!(retrieved.name, original.name);
474        assert_eq!(retrieved.description, original.description);
475        assert_eq!(retrieved.org_id, original.org_id);
476        assert_eq!(retrieved.services.len(), original.services.len());
477    }
478
479    #[tokio::test]
480    async fn test_get_federation_not_found() {
481        let (db, _temp_dir) = create_test_db().await;
482        let non_existent_id = Uuid::new_v4();
483
484        let result = db.get_federation(&non_existent_id).await.unwrap();
485        assert!(result.is_none());
486    }
487
488    #[tokio::test]
489    async fn test_get_federation_with_services() {
490        let (db, _temp_dir) = create_test_db().await;
491        let original = create_test_federation();
492        let federation_id = original.id;
493
494        db.create_federation(&original).await.unwrap();
495
496        let retrieved = db.get_federation(&federation_id).await.unwrap().unwrap();
497
498        assert_eq!(retrieved.services.len(), 2);
499        assert_eq!(retrieved.services[0].name, "auth");
500        assert_eq!(retrieved.services[0].base_path, "/auth");
501        assert_eq!(retrieved.services[0].reality_level, ServiceRealityLevel::Real);
502        assert_eq!(retrieved.services[1].name, "payments");
503        assert_eq!(retrieved.services[1].reality_level, ServiceRealityLevel::MockV3);
504    }
505
506    #[tokio::test]
507    async fn test_get_federation_preserves_service_config() {
508        let (db, _temp_dir) = create_test_db().await;
509        let original = create_test_federation();
510        let federation_id = original.id;
511
512        db.create_federation(&original).await.unwrap();
513
514        let retrieved = db.get_federation(&federation_id).await.unwrap().unwrap();
515
516        // Check that config was preserved
517        assert_eq!(retrieved.services[0].config.get("timeout"), Some(&serde_json::json!(5000)));
518    }
519
520    #[tokio::test]
521    async fn test_get_federation_preserves_service_dependencies() {
522        let (db, _temp_dir) = create_test_db().await;
523        let original = create_test_federation();
524        let federation_id = original.id;
525
526        db.create_federation(&original).await.unwrap();
527
528        let retrieved = db.get_federation(&federation_id).await.unwrap().unwrap();
529
530        // Check that dependencies were preserved
531        assert_eq!(retrieved.services[0].dependencies, vec!["database".to_string()]);
532    }
533
534    #[tokio::test]
535    async fn test_list_federations() {
536        let (db, _temp_dir) = create_test_db().await;
537        let org_id = Uuid::new_v4();
538
539        // Create multiple federations for the same org
540        for i in 0..3 {
541            let mut federation = create_test_federation();
542            federation.org_id = org_id;
543            federation.name = format!("federation-{i}");
544            db.create_federation(&federation).await.unwrap();
545        }
546
547        let federations = db.list_federations(&org_id).await.unwrap();
548        assert_eq!(federations.len(), 3);
549    }
550
551    #[tokio::test]
552    async fn test_list_federations_empty() {
553        let (db, _temp_dir) = create_test_db().await;
554        let org_id = Uuid::new_v4();
555
556        let federations = db.list_federations(&org_id).await.unwrap();
557        assert!(federations.is_empty());
558    }
559
560    #[tokio::test]
561    async fn test_list_federations_filters_by_org() {
562        let (db, _temp_dir) = create_test_db().await;
563        let org_id1 = Uuid::new_v4();
564        let org_id2 = Uuid::new_v4();
565
566        // Create federation for org1
567        let mut federation1 = create_test_federation();
568        federation1.org_id = org_id1;
569        db.create_federation(&federation1).await.unwrap();
570
571        // Create federation for org2
572        let mut federation2 = create_test_federation();
573        federation2.org_id = org_id2;
574        db.create_federation(&federation2).await.unwrap();
575
576        let org1_feds = db.list_federations(&org_id1).await.unwrap();
577        assert_eq!(org1_feds.len(), 1);
578        assert_eq!(org1_feds[0].id, federation1.id);
579
580        let org2_feds = db.list_federations(&org_id2).await.unwrap();
581        assert_eq!(org2_feds.len(), 1);
582        assert_eq!(org2_feds[0].id, federation2.id);
583    }
584
585    #[tokio::test]
586    async fn test_list_federations_with_services() {
587        let (db, _temp_dir) = create_test_db().await;
588        let org_id = Uuid::new_v4();
589
590        let mut federation = create_test_federation();
591        federation.org_id = org_id;
592        db.create_federation(&federation).await.unwrap();
593
594        let federations = db.list_federations(&org_id).await.unwrap();
595        assert_eq!(federations.len(), 1);
596        assert_eq!(federations[0].services.len(), 2);
597    }
598
599    #[tokio::test]
600    async fn test_update_federation() {
601        let (db, _temp_dir) = create_test_db().await;
602        let mut federation = create_test_federation();
603        let federation_id = federation.id;
604
605        // Create initial federation
606        db.create_federation(&federation).await.unwrap();
607
608        // Update the federation
609        federation.name = "updated-federation".to_string();
610        federation.description = "Updated description".to_string();
611
612        db.update_federation(&federation).await.unwrap();
613
614        // Verify the update
615        let updated = db.get_federation(&federation_id).await.unwrap().unwrap();
616        assert_eq!(updated.name, "updated-federation");
617        assert_eq!(updated.description, "Updated description");
618    }
619
620    #[tokio::test]
621    async fn test_update_federation_updates_services() {
622        let (db, _temp_dir) = create_test_db().await;
623        let mut federation = create_test_federation();
624        let federation_id = federation.id;
625
626        // Create initial federation
627        db.create_federation(&federation).await.unwrap();
628
629        // Add a new service
630        federation.services.push(ServiceBoundary::new(
631            "inventory".to_string(),
632            Uuid::new_v4(),
633            "/inventory".to_string(),
634            ServiceRealityLevel::Blended,
635        ));
636
637        db.update_federation(&federation).await.unwrap();
638
639        // Verify services were updated
640        let updated = db.get_federation(&federation_id).await.unwrap().unwrap();
641        assert_eq!(updated.services.len(), 3);
642        assert!(updated.services.iter().any(|s| s.name == "inventory"));
643    }
644
645    #[tokio::test]
646    async fn test_update_federation_removes_old_services() {
647        let (db, _temp_dir) = create_test_db().await;
648        let mut federation = create_test_federation();
649        let federation_id = federation.id;
650
651        // Create initial federation
652        db.create_federation(&federation).await.unwrap();
653
654        // Remove all services
655        federation.services.clear();
656
657        db.update_federation(&federation).await.unwrap();
658
659        // Verify services were removed
660        let updated = db.get_federation(&federation_id).await.unwrap().unwrap();
661        assert!(updated.services.is_empty());
662    }
663
664    #[tokio::test]
665    async fn test_delete_federation() {
666        let (db, _temp_dir) = create_test_db().await;
667        let federation = create_test_federation();
668        let federation_id = federation.id;
669
670        // Create federation
671        db.create_federation(&federation).await.unwrap();
672
673        // Delete it
674        db.delete_federation(&federation_id).await.unwrap();
675
676        // Verify it's gone
677        let result = db.get_federation(&federation_id).await.unwrap();
678        assert!(result.is_none());
679    }
680
681    #[tokio::test]
682    async fn test_delete_federation_cascades_services() {
683        let (db, _temp_dir) = create_test_db().await;
684        let federation = create_test_federation();
685        let federation_id = federation.id;
686
687        // Create federation
688        db.create_federation(&federation).await.unwrap();
689
690        // Delete it
691        db.delete_federation(&federation_id).await.unwrap();
692
693        // Verify services were also deleted
694        let rows = sqlx::query("SELECT COUNT(*) FROM federation_services WHERE federation_id = ?1")
695            .bind(federation_id.to_string())
696            .fetch_one(&db.pool)
697            .await
698            .unwrap();
699
700        let count: i64 = rows.get(0);
701        assert_eq!(count, 0);
702    }
703
704    #[tokio::test]
705    async fn test_create_federation_service_internal() {
706        let (db, _temp_dir) = create_test_db().await;
707        let federation = create_test_federation();
708        let federation_id = federation.id.to_string();
709
710        // First create the federation without services
711        sqlx::query(
712            r"
713            INSERT INTO federations (id, name, org_id, description, created_at, updated_at)
714            VALUES (?1, ?2, ?3, ?4, ?5, ?6)
715            ",
716        )
717        .bind(&federation_id)
718        .bind("test")
719        .bind(federation.org_id.to_string())
720        .bind("")
721        .bind(Utc::now().timestamp())
722        .bind(Utc::now().timestamp())
723        .execute(&db.pool)
724        .await
725        .unwrap();
726
727        // Now test creating a service
728        let service = ServiceBoundary::new(
729            "test-service".to_string(),
730            Uuid::new_v4(),
731            "/test".to_string(),
732            ServiceRealityLevel::Real,
733        );
734
735        let result = db.create_federation_service(&federation_id, &service).await;
736        assert!(result.is_ok());
737
738        // Verify it was created
739        let rows = sqlx::query("SELECT COUNT(*) FROM federation_services WHERE federation_id = ?1")
740            .bind(&federation_id)
741            .fetch_one(&db.pool)
742            .await
743            .unwrap();
744
745        let count: i64 = rows.get(0);
746        assert_eq!(count, 1);
747    }
748
749    #[tokio::test]
750    async fn test_get_federation_services_internal() {
751        let (db, _temp_dir) = create_test_db().await;
752        let federation = create_test_federation();
753        let federation_id = federation.id;
754
755        db.create_federation(&federation).await.unwrap();
756
757        let services = db.get_federation_services(&federation_id.to_string()).await.unwrap();
758        assert_eq!(services.len(), 2);
759
760        // Services should be ordered by base_path
761        assert_eq!(services[0].base_path, "/auth");
762        assert_eq!(services[1].base_path, "/payments");
763    }
764
765    #[tokio::test]
766    async fn test_get_federation_services_empty() {
767        let (db, _temp_dir) = create_test_db().await;
768        let federation_id = Uuid::new_v4().to_string();
769
770        let services = db.get_federation_services(&federation_id).await.unwrap();
771        assert!(services.is_empty());
772    }
773
774    #[tokio::test]
775    async fn test_federation_timestamps() {
776        let (db, _temp_dir) = create_test_db().await;
777        let federation = create_test_federation();
778        let federation_id = federation.id;
779
780        db.create_federation(&federation).await.unwrap();
781
782        let retrieved = db.get_federation(&federation_id).await.unwrap().unwrap();
783
784        // Timestamps should be set
785        assert!(retrieved.created_at.timestamp() > 0);
786        assert!(retrieved.updated_at.timestamp() > 0);
787    }
788
789    #[tokio::test]
790    async fn test_service_reality_level_persistence() {
791        let (db, _temp_dir) = create_test_db().await;
792        let org_id = Uuid::new_v4();
793
794        // Test all reality levels
795        let reality_levels = [
796            ServiceRealityLevel::Real,
797            ServiceRealityLevel::MockV3,
798            ServiceRealityLevel::Blended,
799            ServiceRealityLevel::ChaosDriven,
800        ];
801
802        for (i, level) in reality_levels.iter().enumerate() {
803            let service = ServiceBoundary::new(
804                format!("service-{i}"),
805                Uuid::new_v4(),
806                format!("/service{i}"),
807                *level,
808            );
809
810            let federation = Federation {
811                id: Uuid::new_v4(),
812                name: format!("fed-{i}"),
813                description: String::new(),
814                org_id,
815                services: vec![service],
816                created_at: Utc::now(),
817                updated_at: Utc::now(),
818            };
819
820            db.create_federation(&federation).await.unwrap();
821
822            let retrieved = db.get_federation(&federation.id).await.unwrap().unwrap();
823            assert_eq!(retrieved.services[0].reality_level, *level);
824        }
825    }
826
827    #[tokio::test]
828    async fn test_multiple_federations_same_org() {
829        let (db, _temp_dir) = create_test_db().await;
830        let org_id = Uuid::new_v4();
831
832        // Create 5 federations
833        for i in 0..5 {
834            let mut federation = create_test_federation();
835            federation.org_id = org_id;
836            federation.name = format!("federation-{i}");
837            db.create_federation(&federation).await.unwrap();
838        }
839
840        let federations = db.list_federations(&org_id).await.unwrap();
841        assert_eq!(federations.len(), 5);
842
843        // Should be ordered by created_at DESC
844        for i in 0..4 {
845            assert!(federations[i].created_at >= federations[i + 1].created_at);
846        }
847    }
848
849    #[tokio::test]
850    async fn test_complex_service_config_persistence() {
851        let (db, _temp_dir) = create_test_db().await;
852        let org_id = Uuid::new_v4();
853
854        let mut service = ServiceBoundary::new(
855            "complex".to_string(),
856            Uuid::new_v4(),
857            "/complex".to_string(),
858            ServiceRealityLevel::Blended,
859        );
860
861        // Add complex config
862        service.config.insert("timeout".to_string(), serde_json::json!(5000));
863        service.config.insert("retries".to_string(), serde_json::json!(3));
864        service.config.insert(
865            "features".to_string(),
866            serde_json::json!({
867                "auth": true,
868                "metrics": false,
869                "tracing": true
870            }),
871        );
872        service.config.insert(
873            "endpoints".to_string(),
874            serde_json::json!(["/api/users", "/api/posts", "/api/comments"]),
875        );
876
877        // Add dependencies
878        service.dependencies = vec![
879            "auth".to_string(),
880            "database".to_string(),
881            "cache".to_string(),
882        ];
883
884        let federation = Federation {
885            id: Uuid::new_v4(),
886            name: "complex-test".to_string(),
887            description: String::new(),
888            org_id,
889            services: vec![service],
890            created_at: Utc::now(),
891            updated_at: Utc::now(),
892        };
893
894        db.create_federation(&federation).await.unwrap();
895
896        let retrieved = db.get_federation(&federation.id).await.unwrap().unwrap();
897        let service = &retrieved.services[0];
898
899        // Verify complex config was preserved
900        assert_eq!(service.config.get("timeout"), Some(&serde_json::json!(5000)));
901        assert_eq!(service.config.get("retries"), Some(&serde_json::json!(3)));
902        assert!(service.config.contains_key("features"));
903        assert!(service.config.contains_key("endpoints"));
904
905        // Verify dependencies
906        assert_eq!(service.dependencies.len(), 3);
907        assert!(service.dependencies.contains(&"auth".to_string()));
908        assert!(service.dependencies.contains(&"database".to_string()));
909        assert!(service.dependencies.contains(&"cache".to_string()));
910    }
911}