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, FederationConfig, FederationService};
353    use crate::service::{ServiceBoundary, ServiceRealityLevel};
354    use std::collections::HashMap;
355    use tempfile::TempDir;
356
357    async fn create_test_db() -> (FederationDatabase, TempDir) {
358        let temp_dir = TempDir::new().unwrap();
359        let db_path = temp_dir.path().join("test.db");
360        let db_url = format!("sqlite://{}?mode=rwc", db_path.display());
361
362        let pool = SqlitePool::connect(&db_url).await.unwrap();
363        let db = FederationDatabase::new(pool).await.unwrap();
364        db.run_migrations().await.unwrap();
365
366        (db, temp_dir)
367    }
368
369    fn create_test_federation() -> Federation {
370        let org_id = Uuid::new_v4();
371        let workspace_id1 = Uuid::new_v4();
372        let workspace_id2 = Uuid::new_v4();
373
374        let mut service1 = ServiceBoundary::new(
375            "auth".to_string(),
376            workspace_id1,
377            "/auth".to_string(),
378            ServiceRealityLevel::Real,
379        );
380        service1.config.insert("timeout".to_string(), serde_json::json!(5000));
381        service1.dependencies.push("database".to_string());
382
383        let service2 = ServiceBoundary::new(
384            "payments".to_string(),
385            workspace_id2,
386            "/payments".to_string(),
387            ServiceRealityLevel::MockV3,
388        );
389
390        Federation {
391            id: Uuid::new_v4(),
392            name: "test-federation".to_string(),
393            description: "Test federation for unit tests".to_string(),
394            org_id,
395            services: vec![service1, service2],
396            created_at: Utc::now(),
397            updated_at: Utc::now(),
398        }
399    }
400
401    #[tokio::test]
402    async fn test_new_database() {
403        let temp_dir = TempDir::new().unwrap();
404        let db_path = temp_dir.path().join("test.db");
405        let db_url = format!("sqlite://{}?mode=rwc", db_path.display());
406
407        let pool = SqlitePool::connect(&db_url).await.unwrap();
408        let result = FederationDatabase::new(pool).await;
409
410        assert!(result.is_ok());
411    }
412
413    #[tokio::test]
414    async fn test_run_migrations() {
415        let (db, _temp_dir) = create_test_db().await;
416
417        // Verify tables were created by trying to query them
418        let result = sqlx::query("SELECT COUNT(*) FROM federations").fetch_one(&db.pool).await;
419
420        assert!(result.is_ok());
421    }
422
423    #[tokio::test]
424    async fn test_create_federation() {
425        let (db, _temp_dir) = create_test_db().await;
426        let federation = create_test_federation();
427
428        let result = db.create_federation(&federation).await;
429        assert!(result.is_ok());
430
431        // Verify it was inserted
432        let row = sqlx::query("SELECT COUNT(*) FROM federations WHERE id = ?1")
433            .bind(federation.id.to_string())
434            .fetch_one(&db.pool)
435            .await
436            .unwrap();
437
438        let count: i64 = row.get(0);
439        assert_eq!(count, 1);
440    }
441
442    #[tokio::test]
443    async fn test_create_federation_with_services() {
444        let (db, _temp_dir) = create_test_db().await;
445        let federation = create_test_federation();
446        let federation_id = federation.id;
447
448        db.create_federation(&federation).await.unwrap();
449
450        // Verify services were created
451        let rows = sqlx::query("SELECT COUNT(*) FROM federation_services WHERE federation_id = ?1")
452            .bind(federation_id.to_string())
453            .fetch_one(&db.pool)
454            .await
455            .unwrap();
456
457        let count: i64 = rows.get(0);
458        assert_eq!(count, 2);
459    }
460
461    #[tokio::test]
462    async fn test_get_federation() {
463        let (db, _temp_dir) = create_test_db().await;
464        let original = create_test_federation();
465        let federation_id = original.id;
466
467        db.create_federation(&original).await.unwrap();
468
469        let result = db.get_federation(&federation_id).await.unwrap();
470        assert!(result.is_some());
471
472        let retrieved = result.unwrap();
473        assert_eq!(retrieved.id, original.id);
474        assert_eq!(retrieved.name, original.name);
475        assert_eq!(retrieved.description, original.description);
476        assert_eq!(retrieved.org_id, original.org_id);
477        assert_eq!(retrieved.services.len(), original.services.len());
478    }
479
480    #[tokio::test]
481    async fn test_get_federation_not_found() {
482        let (db, _temp_dir) = create_test_db().await;
483        let non_existent_id = Uuid::new_v4();
484
485        let result = db.get_federation(&non_existent_id).await.unwrap();
486        assert!(result.is_none());
487    }
488
489    #[tokio::test]
490    async fn test_get_federation_with_services() {
491        let (db, _temp_dir) = create_test_db().await;
492        let original = create_test_federation();
493        let federation_id = original.id;
494
495        db.create_federation(&original).await.unwrap();
496
497        let retrieved = db.get_federation(&federation_id).await.unwrap().unwrap();
498
499        assert_eq!(retrieved.services.len(), 2);
500        assert_eq!(retrieved.services[0].name, "auth");
501        assert_eq!(retrieved.services[0].base_path, "/auth");
502        assert_eq!(retrieved.services[0].reality_level, ServiceRealityLevel::Real);
503        assert_eq!(retrieved.services[1].name, "payments");
504        assert_eq!(retrieved.services[1].reality_level, ServiceRealityLevel::MockV3);
505    }
506
507    #[tokio::test]
508    async fn test_get_federation_preserves_service_config() {
509        let (db, _temp_dir) = create_test_db().await;
510        let original = create_test_federation();
511        let federation_id = original.id;
512
513        db.create_federation(&original).await.unwrap();
514
515        let retrieved = db.get_federation(&federation_id).await.unwrap().unwrap();
516
517        // Check that config was preserved
518        assert_eq!(retrieved.services[0].config.get("timeout"), Some(&serde_json::json!(5000)));
519    }
520
521    #[tokio::test]
522    async fn test_get_federation_preserves_service_dependencies() {
523        let (db, _temp_dir) = create_test_db().await;
524        let original = create_test_federation();
525        let federation_id = original.id;
526
527        db.create_federation(&original).await.unwrap();
528
529        let retrieved = db.get_federation(&federation_id).await.unwrap().unwrap();
530
531        // Check that dependencies were preserved
532        assert_eq!(retrieved.services[0].dependencies, vec!["database".to_string()]);
533    }
534
535    #[tokio::test]
536    async fn test_list_federations() {
537        let (db, _temp_dir) = create_test_db().await;
538        let org_id = Uuid::new_v4();
539
540        // Create multiple federations for the same org
541        for i in 0..3 {
542            let mut federation = create_test_federation();
543            federation.org_id = org_id;
544            federation.name = format!("federation-{i}");
545            db.create_federation(&federation).await.unwrap();
546        }
547
548        let federations = db.list_federations(&org_id).await.unwrap();
549        assert_eq!(federations.len(), 3);
550    }
551
552    #[tokio::test]
553    async fn test_list_federations_empty() {
554        let (db, _temp_dir) = create_test_db().await;
555        let org_id = Uuid::new_v4();
556
557        let federations = db.list_federations(&org_id).await.unwrap();
558        assert!(federations.is_empty());
559    }
560
561    #[tokio::test]
562    async fn test_list_federations_filters_by_org() {
563        let (db, _temp_dir) = create_test_db().await;
564        let org_id1 = Uuid::new_v4();
565        let org_id2 = Uuid::new_v4();
566
567        // Create federation for org1
568        let mut federation1 = create_test_federation();
569        federation1.org_id = org_id1;
570        db.create_federation(&federation1).await.unwrap();
571
572        // Create federation for org2
573        let mut federation2 = create_test_federation();
574        federation2.org_id = org_id2;
575        db.create_federation(&federation2).await.unwrap();
576
577        let org1_feds = db.list_federations(&org_id1).await.unwrap();
578        assert_eq!(org1_feds.len(), 1);
579        assert_eq!(org1_feds[0].id, federation1.id);
580
581        let org2_feds = db.list_federations(&org_id2).await.unwrap();
582        assert_eq!(org2_feds.len(), 1);
583        assert_eq!(org2_feds[0].id, federation2.id);
584    }
585
586    #[tokio::test]
587    async fn test_list_federations_with_services() {
588        let (db, _temp_dir) = create_test_db().await;
589        let org_id = Uuid::new_v4();
590
591        let mut federation = create_test_federation();
592        federation.org_id = org_id;
593        db.create_federation(&federation).await.unwrap();
594
595        let federations = db.list_federations(&org_id).await.unwrap();
596        assert_eq!(federations.len(), 1);
597        assert_eq!(federations[0].services.len(), 2);
598    }
599
600    #[tokio::test]
601    async fn test_update_federation() {
602        let (db, _temp_dir) = create_test_db().await;
603        let mut federation = create_test_federation();
604        let federation_id = federation.id;
605
606        // Create initial federation
607        db.create_federation(&federation).await.unwrap();
608
609        // Update the federation
610        federation.name = "updated-federation".to_string();
611        federation.description = "Updated description".to_string();
612
613        db.update_federation(&federation).await.unwrap();
614
615        // Verify the update
616        let updated = db.get_federation(&federation_id).await.unwrap().unwrap();
617        assert_eq!(updated.name, "updated-federation");
618        assert_eq!(updated.description, "Updated description");
619    }
620
621    #[tokio::test]
622    async fn test_update_federation_updates_services() {
623        let (db, _temp_dir) = create_test_db().await;
624        let mut federation = create_test_federation();
625        let federation_id = federation.id;
626
627        // Create initial federation
628        db.create_federation(&federation).await.unwrap();
629
630        // Add a new service
631        federation.services.push(ServiceBoundary::new(
632            "inventory".to_string(),
633            Uuid::new_v4(),
634            "/inventory".to_string(),
635            ServiceRealityLevel::Blended,
636        ));
637
638        db.update_federation(&federation).await.unwrap();
639
640        // Verify services were updated
641        let updated = db.get_federation(&federation_id).await.unwrap().unwrap();
642        assert_eq!(updated.services.len(), 3);
643        assert!(updated.services.iter().any(|s| s.name == "inventory"));
644    }
645
646    #[tokio::test]
647    async fn test_update_federation_removes_old_services() {
648        let (db, _temp_dir) = create_test_db().await;
649        let mut federation = create_test_federation();
650        let federation_id = federation.id;
651
652        // Create initial federation
653        db.create_federation(&federation).await.unwrap();
654
655        // Remove all services
656        federation.services.clear();
657
658        db.update_federation(&federation).await.unwrap();
659
660        // Verify services were removed
661        let updated = db.get_federation(&federation_id).await.unwrap().unwrap();
662        assert!(updated.services.is_empty());
663    }
664
665    #[tokio::test]
666    async fn test_delete_federation() {
667        let (db, _temp_dir) = create_test_db().await;
668        let federation = create_test_federation();
669        let federation_id = federation.id;
670
671        // Create federation
672        db.create_federation(&federation).await.unwrap();
673
674        // Delete it
675        db.delete_federation(&federation_id).await.unwrap();
676
677        // Verify it's gone
678        let result = db.get_federation(&federation_id).await.unwrap();
679        assert!(result.is_none());
680    }
681
682    #[tokio::test]
683    async fn test_delete_federation_cascades_services() {
684        let (db, _temp_dir) = create_test_db().await;
685        let federation = create_test_federation();
686        let federation_id = federation.id;
687
688        // Create federation
689        db.create_federation(&federation).await.unwrap();
690
691        // Delete it
692        db.delete_federation(&federation_id).await.unwrap();
693
694        // Verify services were also deleted
695        let rows = sqlx::query("SELECT COUNT(*) FROM federation_services WHERE federation_id = ?1")
696            .bind(federation_id.to_string())
697            .fetch_one(&db.pool)
698            .await
699            .unwrap();
700
701        let count: i64 = rows.get(0);
702        assert_eq!(count, 0);
703    }
704
705    #[tokio::test]
706    async fn test_create_federation_service_internal() {
707        let (db, _temp_dir) = create_test_db().await;
708        let federation = create_test_federation();
709        let federation_id = federation.id.to_string();
710
711        // First create the federation without services
712        sqlx::query(
713            r"
714            INSERT INTO federations (id, name, org_id, description, created_at, updated_at)
715            VALUES (?1, ?2, ?3, ?4, ?5, ?6)
716            ",
717        )
718        .bind(&federation_id)
719        .bind("test")
720        .bind(federation.org_id.to_string())
721        .bind("")
722        .bind(Utc::now().timestamp())
723        .bind(Utc::now().timestamp())
724        .execute(&db.pool)
725        .await
726        .unwrap();
727
728        // Now test creating a service
729        let service = ServiceBoundary::new(
730            "test-service".to_string(),
731            Uuid::new_v4(),
732            "/test".to_string(),
733            ServiceRealityLevel::Real,
734        );
735
736        let result = db.create_federation_service(&federation_id, &service).await;
737        assert!(result.is_ok());
738
739        // Verify it was created
740        let rows = sqlx::query("SELECT COUNT(*) FROM federation_services WHERE federation_id = ?1")
741            .bind(&federation_id)
742            .fetch_one(&db.pool)
743            .await
744            .unwrap();
745
746        let count: i64 = rows.get(0);
747        assert_eq!(count, 1);
748    }
749
750    #[tokio::test]
751    async fn test_get_federation_services_internal() {
752        let (db, _temp_dir) = create_test_db().await;
753        let federation = create_test_federation();
754        let federation_id = federation.id;
755
756        db.create_federation(&federation).await.unwrap();
757
758        let services = db.get_federation_services(&federation_id.to_string()).await.unwrap();
759        assert_eq!(services.len(), 2);
760
761        // Services should be ordered by base_path
762        assert_eq!(services[0].base_path, "/auth");
763        assert_eq!(services[1].base_path, "/payments");
764    }
765
766    #[tokio::test]
767    async fn test_get_federation_services_empty() {
768        let (db, _temp_dir) = create_test_db().await;
769        let federation_id = Uuid::new_v4().to_string();
770
771        let services = db.get_federation_services(&federation_id).await.unwrap();
772        assert!(services.is_empty());
773    }
774
775    #[tokio::test]
776    async fn test_federation_timestamps() {
777        let (db, _temp_dir) = create_test_db().await;
778        let federation = create_test_federation();
779        let federation_id = federation.id;
780
781        db.create_federation(&federation).await.unwrap();
782
783        let retrieved = db.get_federation(&federation_id).await.unwrap().unwrap();
784
785        // Timestamps should be set
786        assert!(retrieved.created_at.timestamp() > 0);
787        assert!(retrieved.updated_at.timestamp() > 0);
788    }
789
790    #[tokio::test]
791    async fn test_service_reality_level_persistence() {
792        let (db, _temp_dir) = create_test_db().await;
793        let org_id = Uuid::new_v4();
794
795        // Test all reality levels
796        let reality_levels = [
797            ServiceRealityLevel::Real,
798            ServiceRealityLevel::MockV3,
799            ServiceRealityLevel::Blended,
800            ServiceRealityLevel::ChaosDriven,
801        ];
802
803        for (i, level) in reality_levels.iter().enumerate() {
804            let service = ServiceBoundary::new(
805                format!("service-{i}"),
806                Uuid::new_v4(),
807                format!("/service{i}"),
808                *level,
809            );
810
811            let federation = Federation {
812                id: Uuid::new_v4(),
813                name: format!("fed-{i}"),
814                description: String::new(),
815                org_id,
816                services: vec![service],
817                created_at: Utc::now(),
818                updated_at: Utc::now(),
819            };
820
821            db.create_federation(&federation).await.unwrap();
822
823            let retrieved = db.get_federation(&federation.id).await.unwrap().unwrap();
824            assert_eq!(retrieved.services[0].reality_level, *level);
825        }
826    }
827
828    #[tokio::test]
829    async fn test_multiple_federations_same_org() {
830        let (db, _temp_dir) = create_test_db().await;
831        let org_id = Uuid::new_v4();
832
833        // Create 5 federations
834        for i in 0..5 {
835            let mut federation = create_test_federation();
836            federation.org_id = org_id;
837            federation.name = format!("federation-{i}");
838            db.create_federation(&federation).await.unwrap();
839        }
840
841        let federations = db.list_federations(&org_id).await.unwrap();
842        assert_eq!(federations.len(), 5);
843
844        // Should be ordered by created_at DESC
845        for i in 0..4 {
846            assert!(federations[i].created_at >= federations[i + 1].created_at);
847        }
848    }
849
850    #[tokio::test]
851    async fn test_complex_service_config_persistence() {
852        let (db, _temp_dir) = create_test_db().await;
853        let org_id = Uuid::new_v4();
854
855        let mut service = ServiceBoundary::new(
856            "complex".to_string(),
857            Uuid::new_v4(),
858            "/complex".to_string(),
859            ServiceRealityLevel::Blended,
860        );
861
862        // Add complex config
863        service.config.insert("timeout".to_string(), serde_json::json!(5000));
864        service.config.insert("retries".to_string(), serde_json::json!(3));
865        service.config.insert(
866            "features".to_string(),
867            serde_json::json!({
868                "auth": true,
869                "metrics": false,
870                "tracing": true
871            }),
872        );
873        service.config.insert(
874            "endpoints".to_string(),
875            serde_json::json!(["/api/users", "/api/posts", "/api/comments"]),
876        );
877
878        // Add dependencies
879        service.dependencies = vec![
880            "auth".to_string(),
881            "database".to_string(),
882            "cache".to_string(),
883        ];
884
885        let federation = Federation {
886            id: Uuid::new_v4(),
887            name: "complex-test".to_string(),
888            description: String::new(),
889            org_id,
890            services: vec![service],
891            created_at: Utc::now(),
892            updated_at: Utc::now(),
893        };
894
895        db.create_federation(&federation).await.unwrap();
896
897        let retrieved = db.get_federation(&federation.id).await.unwrap().unwrap();
898        let service = &retrieved.services[0];
899
900        // Verify complex config was preserved
901        assert_eq!(service.config.get("timeout"), Some(&serde_json::json!(5000)));
902        assert_eq!(service.config.get("retries"), Some(&serde_json::json!(3)));
903        assert!(service.config.contains_key("features"));
904        assert!(service.config.contains_key("endpoints"));
905
906        // Verify dependencies
907        assert_eq!(service.dependencies.len(), 3);
908        assert!(service.dependencies.contains(&"auth".to_string()));
909        assert!(service.dependencies.contains(&"database".to_string()));
910        assert!(service.dependencies.contains(&"cache".to_string()));
911    }
912}