mod harness; use charybdis::charybdis::core::{ComponentMetadata, ComponentSpec}; use charybdis::charybdis::entities::Entity; use charybdis::charybdis::entities::entity::{Metadata, Spec}; use harness::TestDb; fn test_component(name: &str) -> Entity { Entity { kind: "Component".to_string(), metadata: Some(Metadata::ComponentMetadata(ComponentMetadata { name: name.to_string(), namespace: "default".to_string(), description: format!("{} service", name), ..Default::default() })), spec: Some(Spec::ComponentSpec(ComponentSpec { r#type: "service".to_string(), lifecycle: "production".to_string(), owner: "team-platform".to_string(), ..Default::default() })), ..Default::default() } } #[tokio::test] async fn create_and_get_entity() { let db = TestDb::new().await; let created = db.repository.create(&test_component("payment-api")).await.unwrap(); assert!(!created.id.is_empty()); assert_eq!(created.kind, "Component"); assert!(created.created_at.is_some()); assert!(created.updated_at.is_some()); let fetched = db.repository.get_by_id(&created.id).await.unwrap().unwrap(); assert_eq!(fetched.id, created.id); assert_eq!(fetched.kind, "Component"); } #[tokio::test] async fn get_nonexistent_returns_none() { let db = TestDb::new().await; let result = db .repository .get_by_id("00000000-0000-0000-0000-000000000000") .await .unwrap(); assert!(result.is_none()); } #[tokio::test] async fn update_entity() { let db = TestDb::new().await; let created = db.repository.create(&test_component("auth-svc")).await.unwrap(); let mut updated_data = test_component("auth-svc"); if let Some(Metadata::ComponentMetadata(ref mut m)) = updated_data.metadata { m.description = "Updated auth service".to_string(); } let updated = db .repository .update(&created.id, &updated_data) .await .unwrap() .unwrap(); assert_eq!(updated.id, created.id); assert_eq!(updated.created_at, created.created_at); assert_ne!(updated.updated_at, created.updated_at); if let Some(Metadata::ComponentMetadata(m)) = &updated.metadata { assert_eq!(m.description, "Updated auth service"); } else { panic!("Expected ComponentMetadata"); } } #[tokio::test] async fn delete_entity() { let db = TestDb::new().await; let created = db.repository.create(&test_component("to-delete")).await.unwrap(); let deleted = db.repository.delete(&created.id).await.unwrap(); assert!(deleted); let gone = db.repository.get_by_id(&created.id).await.unwrap(); assert!(gone.is_none()); // Double delete returns false let deleted_again = db.repository.delete(&created.id).await.unwrap(); assert!(!deleted_again); } #[tokio::test] async fn list_paginated() { let db = TestDb::new().await; for i in 0..5 { db.repository .create(&test_component(&format!("svc-{}", i))) .await .unwrap(); } // Page 1: get 2 entities let page1 = db .repository .list_paginated(Some("Component"), None, 2, None) .await .unwrap(); assert_eq!(page1.entities.len(), 2); assert_eq!(page1.total_count, 5); assert!(page1.next_page_token.is_some()); // Page 2: next 2 let page2 = db .repository .list_paginated(Some("Component"), None, 2, page1.next_page_token.as_deref()) .await .unwrap(); assert_eq!(page2.entities.len(), 2); assert!(page2.next_page_token.is_some()); // Page 3: last 1 let page3 = db .repository .list_paginated(Some("Component"), None, 2, page2.next_page_token.as_deref()) .await .unwrap(); assert_eq!(page3.entities.len(), 1); assert!(page3.next_page_token.is_none()); // No duplicates across pages let all_ids: Vec = page1 .entities .iter() .chain(page2.entities.iter()) .chain(page3.entities.iter()) .map(|e| e.id.clone()) .collect(); let unique: std::collections::HashSet<&String> = all_ids.iter().collect(); assert_eq!(all_ids.len(), unique.len()); } #[tokio::test] async fn get_by_kind_and_name() { let db = TestDb::new().await; db.repository.create(&test_component("unique-svc")).await.unwrap(); db.repository.create(&test_component("other-svc")).await.unwrap(); let found = db .repository .get_by_kind_and_name("Component", "unique-svc") .await .unwrap(); assert!(found.is_some()); if let Some(Metadata::ComponentMetadata(m)) = &found.unwrap().metadata { assert_eq!(m.name, "unique-svc"); } let not_found = db .repository .get_by_kind_and_name("Component", "nonexistent") .await .unwrap(); assert!(not_found.is_none()); } #[tokio::test] async fn atomic_annotation_update() { let db = TestDb::new().await; let created = db.repository.create(&test_component("annotated-svc")).await.unwrap(); // First annotation update let mut annotations1 = std::collections::HashMap::new(); annotations1.insert("defectdojo.com/product-id".to_string(), "123".to_string()); let updated = db .repository .update_annotations(&created.id, annotations1) .await .unwrap(); assert!(updated); // Second annotation update (should merge, not overwrite) let mut annotations2 = std::collections::HashMap::new(); annotations2.insert("github.com/repo".to_string(), "org/repo".to_string()); db.repository .update_annotations(&created.id, annotations2) .await .unwrap(); // Verify both annotations exist let entity = db.repository.get_by_id(&created.id).await.unwrap().unwrap(); assert_eq!(entity.annotations.get("defectdojo.com/product-id").unwrap(), "123"); assert_eq!(entity.annotations.get("github.com/repo").unwrap(), "org/repo"); }