initial-commit

This commit is contained in:
Guillaume GRABÉ
2026-05-12 17:06:43 +02:00
commit 051a080dfa
110 changed files with 26377 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
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<String> = 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");
}
+31
View File
@@ -0,0 +1,31 @@
use charybdis::database::{EntityRepository, ensure_schema};
use sqlx::PgPool;
use testcontainers::runners::AsyncRunner;
use testcontainers_modules::postgres::Postgres;
pub struct TestDb {
pub pool: PgPool,
pub repository: EntityRepository,
_container: testcontainers::ContainerAsync<Postgres>,
}
impl TestDb {
pub async fn new() -> Self {
// Testcontainers picks up DOCKER_HOST or TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE
// For Colima users: export DOCKER_HOST=unix:///Users/$USER/.colima/default/docker.sock
let container = Postgres::default().start().await.unwrap();
let port = container.get_host_port_ipv4(5432).await.unwrap();
let url = format!("postgresql://postgres:postgres@127.0.0.1:{}/postgres", port);
let pool = PgPool::connect(&url).await.unwrap();
ensure_schema(&pool).await.unwrap();
let repository = EntityRepository::new(pool.clone());
Self {
pool,
repository,
_container: container,
}
}
}