diff --git a/crates/vil_db_cassandra/Cargo.toml b/crates/vil_db_cassandra/Cargo.toml index 6d80f474..b38140ad 100644 --- a/crates/vil_db_cassandra/Cargo.toml +++ b/crates/vil_db_cassandra/Cargo.toml @@ -17,3 +17,7 @@ vil_connector_macros = { workspace = true } tokio = { workspace = true } serde = { workspace = true } scylla = "0.14" + +[dev-dependencies] +serde_json = "1" +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/vil_db_cassandra/tests/integration.rs b/crates/vil_db_cassandra/tests/integration.rs new file mode 100644 index 00000000..4be18a5a --- /dev/null +++ b/crates/vil_db_cassandra/tests/integration.rs @@ -0,0 +1,25 @@ +//! Integration tests for `vil_db_cassandra` — require a live Cassandra/ScyllaDB. +//! +//! These tests are `#[ignore]` by default. To run: +//! ```bash +//! docker run -d --name scylla -p 9042:9042 scylladb/scylla +//! VIL_CASSANDRA_ADDR=127.0.0.1:9042 VIL_CASSANDRA_KEYSPACE=system \ +//! cargo test -p vil_db_cassandra --test integration -- --ignored +//! ``` + +use vil_db_cassandra::{CassandraClient, CassandraConfig}; + +fn addr() -> String { + std::env::var("VIL_CASSANDRA_ADDR").unwrap_or_else(|_| "127.0.0.1:9042".to_string()) +} +fn keyspace() -> String { + std::env::var("VIL_CASSANDRA_KEYSPACE").unwrap_or_else(|_| "system".to_string()) +} + +#[tokio::test] +#[ignore = "requires a live Cassandra/ScyllaDB node"] +async fn connects_to_cluster() { + let cfg = CassandraConfig::new(addr(), keyspace()); + let client = CassandraClient::new(cfg).await; + assert!(client.is_ok(), "expected a successful connection"); +} diff --git a/crates/vil_db_cassandra/tests/unit.rs b/crates/vil_db_cassandra/tests/unit.rs new file mode 100644 index 00000000..ff4f6619 --- /dev/null +++ b/crates/vil_db_cassandra/tests/unit.rs @@ -0,0 +1,46 @@ +//! Pure unit tests for `vil_db_cassandra` — no live cluster required. +//! Run: `cargo test -p vil_db_cassandra` + +use vil_db_cassandra::{CassandraConfig, CassandraFault}; + +#[test] +fn config_new_sets_single_contact_point_and_defaults() { + let cfg = CassandraConfig::new("127.0.0.1:9042", "myapp"); + assert_eq!(cfg.contact_points, vec!["127.0.0.1:9042".to_string()]); + assert_eq!(cfg.keyspace, "myapp"); + assert_eq!(cfg.pool_id, 0); +} + +#[test] +fn config_struct_literal_supports_multiple_contact_points() { + let cfg = CassandraConfig { + contact_points: vec!["a:9042".into(), "b:9042".into()], + keyspace: "k".into(), + pool_id: 7, + }; + assert_eq!(cfg.contact_points.len(), 2); + assert_eq!(cfg.pool_id, 7); +} + +#[test] +fn config_serde_round_trip() { + let cfg = CassandraConfig::new("10.0.0.1:9042", "ks"); + let json = serde_json::to_string(&cfg).expect("serialize"); + let back: CassandraConfig = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(back.contact_points, cfg.contact_points); + assert_eq!(back.keyspace, cfg.keyspace); + assert_eq!(back.pool_id, cfg.pool_id); +} + +#[test] +fn fault_metadata_is_consistent() { + let conn = CassandraFault::ConnectionFailed { uri_hash: 1, reason_code: 2 }; + assert_eq!(conn.kind(), "ConnectionFailed"); + assert!(conn.is_retryable()); + assert!(conn.error_code() >= 1); + + let q = CassandraFault::QueryFailed { query_hash: 9, reason_code: 7 }; + assert_eq!(q.kind(), "QueryFailed"); + assert!(!q.is_retryable()); + assert_ne!(conn.error_code(), q.error_code()); +} diff --git a/crates/vil_db_clickhouse/Cargo.toml b/crates/vil_db_clickhouse/Cargo.toml index 7986e439..39a6bc6c 100644 --- a/crates/vil_db_clickhouse/Cargo.toml +++ b/crates/vil_db_clickhouse/Cargo.toml @@ -17,3 +17,6 @@ vil_connector_macros = { workspace = true } clickhouse = { version = "0.12", features = ["inserter"] } tokio = { workspace = true } serde = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/vil_db_clickhouse/tests/integration.rs b/crates/vil_db_clickhouse/tests/integration.rs new file mode 100644 index 00000000..f30184b3 --- /dev/null +++ b/crates/vil_db_clickhouse/tests/integration.rs @@ -0,0 +1,22 @@ +//! Integration tests for `vil_db_clickhouse` — require a live ClickHouse. +//! +//! `#[ignore]` by default. To run: +//! ```bash +//! docker run -d --name clickhouse -p 8123:8123 clickhouse/clickhouse-server +//! VIL_CLICKHOUSE_URL=http://localhost:8123 \ +//! cargo test -p vil_db_clickhouse --test integration -- --ignored +//! ``` + +use vil_db_clickhouse::{ChClient, ClickHouseConfig}; + +#[tokio::test] +#[ignore = "requires a live ClickHouse server"] +async fn connects_and_executes() { + let mut cfg = ClickHouseConfig::default(); + if let Ok(url) = std::env::var("VIL_CLICKHOUSE_URL") { + cfg.url = url; + } + let client = ChClient::new(cfg); + let res = client.execute("SELECT 1").await; + assert!(res.is_ok(), "expected query to succeed against live ClickHouse"); +} diff --git a/crates/vil_db_clickhouse/tests/unit.rs b/crates/vil_db_clickhouse/tests/unit.rs new file mode 100644 index 00000000..89cb531e --- /dev/null +++ b/crates/vil_db_clickhouse/tests/unit.rs @@ -0,0 +1,37 @@ +//! Pure unit tests for `vil_db_clickhouse` — no live server required. +//! Run: `cargo test -p vil_db_clickhouse` + +use vil_db_clickhouse::{ChFault, ClickHouseConfig}; + +#[test] +fn default_config_values() { + let cfg = ClickHouseConfig::default(); + assert_eq!(cfg.url, "http://localhost:8123"); + assert_eq!(cfg.database, "default"); + assert!(cfg.username.is_none()); + assert!(cfg.password.is_none()); +} + +#[test] +fn struct_literal_with_auth() { + let cfg = ClickHouseConfig { + url: "http://ch:8123".into(), + database: "analytics".into(), + username: Some("default".into()), + password: None, + }; + assert_eq!(cfg.database, "analytics"); + assert_eq!(cfg.username.as_deref(), Some("default")); +} + +#[test] +fn fault_metadata_is_consistent() { + let to = ChFault::Timeout { operation_hash: 1, elapsed_ms: 10 }; + assert_eq!(to.kind(), "Timeout"); + assert!(to.is_retryable()); + + let qf = ChFault::QueryFailed { query_hash: 2, reason_code: 3 }; + assert_eq!(qf.kind(), "QueryFailed"); + assert!(!qf.is_retryable()); + assert_ne!(to.error_code(), qf.error_code()); +} diff --git a/crates/vil_db_dynamodb/Cargo.toml b/crates/vil_db_dynamodb/Cargo.toml index 8fcf97a1..b45ee524 100644 --- a/crates/vil_db_dynamodb/Cargo.toml +++ b/crates/vil_db_dynamodb/Cargo.toml @@ -18,3 +18,7 @@ tokio = { workspace = true } serde = { workspace = true } aws-sdk-dynamodb = "1.55" aws-config = { version = "1.5", features = ["behavior-version-latest"] } + +[dev-dependencies] +serde_json = "1" +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/vil_db_dynamodb/tests/integration.rs b/crates/vil_db_dynamodb/tests/integration.rs new file mode 100644 index 00000000..8dc985c5 --- /dev/null +++ b/crates/vil_db_dynamodb/tests/integration.rs @@ -0,0 +1,21 @@ +//! Integration tests for `vil_db_dynamodb` — require a DynamoDB-compatible endpoint. +//! +//! `#[ignore]` by default. To run against LocalStack: +//! ```bash +//! docker run -d --name localstack -p 4566:4566 localstack/localstack +//! VIL_DYNAMO_REGION=us-east-1 VIL_DYNAMO_ENDPOINT=http://localhost:4566 \ +//! cargo test -p vil_db_dynamodb --test integration -- --ignored +//! ``` + +use vil_db_dynamodb::{DynamoClient, DynamoConfig}; + +#[tokio::test] +#[ignore = "requires a live DynamoDB/LocalStack endpoint"] +async fn builds_client() { + let region = std::env::var("VIL_DYNAMO_REGION").unwrap_or_else(|_| "us-east-1".to_string()); + let endpoint = + std::env::var("VIL_DYNAMO_ENDPOINT").unwrap_or_else(|_| "http://localhost:4566".to_string()); + let cfg = DynamoConfig::new(region).with_endpoint(endpoint); + let client = DynamoClient::new(cfg).await; + assert!(client.is_ok(), "expected client construction to succeed"); +} diff --git a/crates/vil_db_dynamodb/tests/unit.rs b/crates/vil_db_dynamodb/tests/unit.rs new file mode 100644 index 00000000..6c853192 --- /dev/null +++ b/crates/vil_db_dynamodb/tests/unit.rs @@ -0,0 +1,40 @@ +//! Pure unit tests for `vil_db_dynamodb` — no AWS endpoint required. +//! Run: `cargo test -p vil_db_dynamodb` + +use vil_db_dynamodb::{DynamoConfig, DynamoFault}; + +#[test] +fn config_new_defaults() { + let cfg = DynamoConfig::new("us-east-1"); + assert_eq!(cfg.region, "us-east-1"); + assert!(cfg.endpoint_url.is_none()); + assert_eq!(cfg.pool_id, 0); +} + +#[test] +fn with_endpoint_overrides_url() { + let cfg = DynamoConfig::new("us-west-2").with_endpoint("http://localhost:4566"); + assert_eq!(cfg.region, "us-west-2"); + assert_eq!(cfg.endpoint_url.as_deref(), Some("http://localhost:4566")); +} + +#[test] +fn config_serde_round_trip() { + let cfg = DynamoConfig::new("eu-west-1").with_endpoint("http://ls:4566"); + let json = serde_json::to_string(&cfg).expect("serialize"); + let back: DynamoConfig = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(back.region, cfg.region); + assert_eq!(back.endpoint_url, cfg.endpoint_url); + assert_eq!(back.pool_id, cfg.pool_id); +} + +#[test] +fn fault_metadata_is_consistent() { + let get = DynamoFault::GetFailed { table_hash: 1, reason_code: 2 }; + assert_eq!(get.kind(), "GetFailed"); + assert!(!get.is_retryable()); + + let cfg_f = DynamoFault::ConfigFailed { reason_code: 9 }; + assert_eq!(cfg_f.kind(), "ConfigFailed"); + assert_ne!(get.error_code(), cfg_f.error_code()); +} diff --git a/crates/vil_db_elastic/Cargo.toml b/crates/vil_db_elastic/Cargo.toml index 30c1e361..93b3bf71 100644 --- a/crates/vil_db_elastic/Cargo.toml +++ b/crates/vil_db_elastic/Cargo.toml @@ -19,3 +19,6 @@ tokio = { workspace = true } serde = { workspace = true } serde_json = "1.0" bytes = "1" + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/vil_db_elastic/tests/integration.rs b/crates/vil_db_elastic/tests/integration.rs new file mode 100644 index 00000000..7f4a9947 --- /dev/null +++ b/crates/vil_db_elastic/tests/integration.rs @@ -0,0 +1,25 @@ +//! Integration tests for `vil_db_elastic` — require a live Elasticsearch/OpenSearch. +//! +//! `#[ignore]` by default. To run: +//! ```bash +//! docker run -d --name es -p 9200:9200 -e discovery.type=single-node \ +//! docker.elastic.co/elasticsearch/elasticsearch:8.15.0 +//! VIL_ELASTIC_URL=http://localhost:9200 \ +//! cargo test -p vil_db_elastic --test integration -- --ignored +//! ``` + +use vil_db_elastic::{ElasticClient, ElasticConfig}; + +#[tokio::test] +#[ignore = "requires a live Elasticsearch/OpenSearch node"] +async fn connects_and_queries() { + let url = std::env::var("VIL_ELASTIC_URL").unwrap_or_else(|_| "http://localhost:9200".to_string()); + let cfg = ElasticConfig { url, username: None, password: None }; + let client = match ElasticClient::new(cfg) { + Ok(c) => c, + Err(_) => panic!("client init failed"), + }; + // Touching the server requires a live node; a missing document simply + // returns an error, which is acceptable for this smoke test. + let _ = client.get("vil-test-index", "missing-doc").await; +} diff --git a/crates/vil_db_elastic/tests/unit.rs b/crates/vil_db_elastic/tests/unit.rs new file mode 100644 index 00000000..1df455c8 --- /dev/null +++ b/crates/vil_db_elastic/tests/unit.rs @@ -0,0 +1,41 @@ +//! Pure unit tests for `vil_db_elastic` — no live node required. +//! Run: `cargo test -p vil_db_elastic` + +use vil_db_elastic::{ElasticConfig, ElasticFault}; + +#[test] +fn default_config_values() { + let cfg = ElasticConfig::default(); + assert_eq!(cfg.url, "http://localhost:9200"); + assert!(cfg.username.is_none()); + assert!(cfg.password.is_none()); +} + +#[test] +fn config_serde_round_trip() { + let cfg = ElasticConfig { + url: "http://es:9200".into(), + username: Some("elastic".into()), + password: Some("changeme".into()), + }; + let json = serde_json::to_string(&cfg).expect("serialize"); + let back: ElasticConfig = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(back.url, cfg.url); + assert_eq!(back.username, cfg.username); + assert_eq!(back.password, cfg.password); +} + +#[test] +fn fault_metadata_is_consistent() { + let conn = ElasticFault::ConnectionFailed { url_hash: 1, reason_code: 2 }; + assert_eq!(conn.kind(), "ConnectionFailed"); + assert!(conn.is_retryable()); + + let nf = ElasticFault::NotFound { index_hash: 1, id_hash: 2 }; + assert_eq!(nf.kind(), "NotFound"); + assert!(!nf.is_retryable()); + + let to = ElasticFault::Timeout { operation_hash: 1, elapsed_ms: 5 }; + assert!(to.is_retryable()); + assert_ne!(conn.error_code(), nf.error_code()); +} diff --git a/crates/vil_db_mongo/Cargo.toml b/crates/vil_db_mongo/Cargo.toml index 508104ba..2b6a54d6 100644 --- a/crates/vil_db_mongo/Cargo.toml +++ b/crates/vil_db_mongo/Cargo.toml @@ -20,3 +20,6 @@ serde = { workspace = true } serde_json = { workspace = true } bson = "2.13" futures-util = "0.3" + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/vil_db_mongo/tests/integration.rs b/crates/vil_db_mongo/tests/integration.rs new file mode 100644 index 00000000..1ba06dd9 --- /dev/null +++ b/crates/vil_db_mongo/tests/integration.rs @@ -0,0 +1,19 @@ +//! Integration tests for `vil_db_mongo` — require a live MongoDB. +//! +//! `#[ignore]` by default. To run: +//! ```bash +//! docker run -d --name mongo -p 27017:27017 mongo:7 +//! VIL_MONGO_URI=mongodb://localhost:27017 VIL_MONGO_DB=vil_test \ +//! cargo test -p vil_db_mongo --test integration -- --ignored +//! ``` + +use vil_db_mongo::{MongoClient, MongoConfig}; + +#[tokio::test] +#[ignore = "requires a live MongoDB instance"] +async fn connects_to_server() { + let uri = std::env::var("VIL_MONGO_URI").unwrap_or_else(|_| "mongodb://localhost:27017".to_string()); + let db = std::env::var("VIL_MONGO_DB").unwrap_or_else(|_| "vil_test".to_string()); + let client = MongoClient::new(MongoConfig::new(uri, db)).await; + assert!(client.is_ok(), "expected a successful connection"); +} diff --git a/crates/vil_db_mongo/tests/unit.rs b/crates/vil_db_mongo/tests/unit.rs new file mode 100644 index 00000000..c5657d0a --- /dev/null +++ b/crates/vil_db_mongo/tests/unit.rs @@ -0,0 +1,43 @@ +//! Pure unit tests for `vil_db_mongo` — no live MongoDB required. +//! Run: `cargo test -p vil_db_mongo` + +use vil_db_mongo::{MongoConfig, MongoFault}; + +#[test] +fn config_new_defaults() { + let cfg = MongoConfig::new("mongodb://localhost:27017", "myapp"); + assert_eq!(cfg.uri, "mongodb://localhost:27017"); + assert_eq!(cfg.database, "myapp"); + assert!(cfg.min_pool.is_none()); + assert!(cfg.max_pool.is_none()); +} + +#[test] +fn config_serde_round_trip() { + let mut cfg = MongoConfig::new("mongodb://h:27017", "db"); + cfg.min_pool = Some(2); + cfg.max_pool = Some(16); + let json = serde_json::to_string(&cfg).expect("serialize"); + let back: MongoConfig = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(back.min_pool, Some(2)); + assert_eq!(back.max_pool, Some(16)); +} + +#[test] +fn deserialize_minimal_json_defaults_pool_to_none() { + let back: MongoConfig = + serde_json::from_str(r#"{"uri":"mongodb://x","database":"d"}"#).expect("deserialize"); + assert!(back.min_pool.is_none()); + assert!(back.max_pool.is_none()); +} + +#[test] +fn fault_metadata_is_consistent() { + let conn = MongoFault::ConnectionFailed { uri_hash: 1, reason_code: 2 }; + assert_eq!(conn.kind(), "ConnectionFailed"); + assert!(conn.is_retryable()); + + let ins = MongoFault::InsertFailed { collection_hash: 3, reason_code: 4 }; + assert_eq!(ins.kind(), "InsertFailed"); + assert!(!ins.is_retryable()); +} diff --git a/crates/vil_db_neo4j/Cargo.toml b/crates/vil_db_neo4j/Cargo.toml index d4ea1d1e..773b6b6d 100644 --- a/crates/vil_db_neo4j/Cargo.toml +++ b/crates/vil_db_neo4j/Cargo.toml @@ -17,3 +17,7 @@ vil_connector_macros = { workspace = true } tokio = { workspace = true } serde = { workspace = true } neo4rs = "0.8" + +[dev-dependencies] +serde_json = "1" +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/vil_db_neo4j/tests/integration.rs b/crates/vil_db_neo4j/tests/integration.rs new file mode 100644 index 00000000..c099b600 --- /dev/null +++ b/crates/vil_db_neo4j/tests/integration.rs @@ -0,0 +1,20 @@ +//! Integration tests for `vil_db_neo4j` — require a live Neo4j. +//! +//! `#[ignore]` by default. To run: +//! ```bash +//! docker run -d --name neo4j -p 7687:7687 -e NEO4J_AUTH=neo4j/password neo4j:5 +//! VIL_NEO4J_URI=bolt://localhost:7687 VIL_NEO4J_USER=neo4j VIL_NEO4J_PASSWORD=password \ +//! cargo test -p vil_db_neo4j --test integration -- --ignored +//! ``` + +use vil_db_neo4j::{Neo4jClient, Neo4jConfig}; + +#[tokio::test] +#[ignore = "requires a live Neo4j instance"] +async fn connects_to_server() { + let uri = std::env::var("VIL_NEO4J_URI").unwrap_or_else(|_| "bolt://localhost:7687".to_string()); + let user = std::env::var("VIL_NEO4J_USER").unwrap_or_else(|_| "neo4j".to_string()); + let password = std::env::var("VIL_NEO4J_PASSWORD").unwrap_or_else(|_| "password".to_string()); + let client = Neo4jClient::new(Neo4jConfig::new(uri, user, password)).await; + assert!(client.is_ok(), "expected a successful connection"); +} diff --git a/crates/vil_db_neo4j/tests/unit.rs b/crates/vil_db_neo4j/tests/unit.rs new file mode 100644 index 00000000..9648ce38 --- /dev/null +++ b/crates/vil_db_neo4j/tests/unit.rs @@ -0,0 +1,36 @@ +//! Pure unit tests for `vil_db_neo4j` — no live database required. +//! Run: `cargo test -p vil_db_neo4j` + +use vil_db_neo4j::{Neo4jConfig, Neo4jFault}; + +#[test] +fn config_new_defaults() { + let cfg = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "password"); + assert_eq!(cfg.uri, "bolt://localhost:7687"); + assert_eq!(cfg.user, "neo4j"); + assert_eq!(cfg.password, "password"); + assert_eq!(cfg.pool_id, 0); +} + +#[test] +fn config_serde_round_trip() { + let cfg = Neo4jConfig::new("bolt://h:7687", "u", "p"); + let json = serde_json::to_string(&cfg).expect("serialize"); + let back: Neo4jConfig = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(back.uri, cfg.uri); + assert_eq!(back.user, cfg.user); + assert_eq!(back.password, cfg.password); + assert_eq!(back.pool_id, cfg.pool_id); +} + +#[test] +fn fault_metadata_is_consistent() { + let conn = Neo4jFault::ConnectionFailed { uri_hash: 1, reason_code: 2 }; + assert_eq!(conn.kind(), "ConnectionFailed"); + assert!(conn.is_retryable()); + + let ex = Neo4jFault::ExecuteFailed { query_hash: 3, reason_code: 4 }; + assert_eq!(ex.kind(), "ExecuteFailed"); + assert!(!ex.is_retryable()); + assert_ne!(conn.error_code(), ex.error_code()); +} diff --git a/crates/vil_db_redis/tests/unit.rs b/crates/vil_db_redis/tests/unit.rs new file mode 100644 index 00000000..56705294 --- /dev/null +++ b/crates/vil_db_redis/tests/unit.rs @@ -0,0 +1,32 @@ +//! Pure unit tests for `vil_db_redis` — no live Redis required. +//! Run: `cargo test -p vil_db_redis` + +use vil_db_redis::RedisConfig; + +#[test] +fn default_config_values() { + let cfg = RedisConfig::default(); + assert_eq!(cfg.url, "redis://127.0.0.1:6379"); + assert_eq!(cfg.max_connections, 20); + assert_eq!(cfg.database, 0); + assert!(cfg.password.is_none()); + assert!(cfg.services.is_empty()); +} + +#[test] +fn new_sets_url_keeps_defaults() { + let cfg = RedisConfig::new("redis://h:6379"); + assert_eq!(cfg.url, "redis://h:6379"); + assert_eq!(cfg.max_connections, 20); + assert_eq!(cfg.database, 0); +} + +#[test] +fn serde_applies_defaults_for_missing_fields() { + let cfg: RedisConfig = + serde_json::from_str(r#"{"url":"redis://x:6379"}"#).expect("deserialize"); + assert_eq!(cfg.url, "redis://x:6379"); + assert_eq!(cfg.max_connections, 20); + assert_eq!(cfg.database, 0); + assert!(cfg.services.is_empty()); +} diff --git a/crates/vil_db_sea_orm/tests/unit.rs b/crates/vil_db_sea_orm/tests/unit.rs new file mode 100644 index 00000000..f863de88 --- /dev/null +++ b/crates/vil_db_sea_orm/tests/unit.rs @@ -0,0 +1,40 @@ +//! Pure unit tests for `vil_db_sea_orm` — no live database required. +//! Run: `cargo test -p vil_db_sea_orm` + +use vil_db_sea_orm::SeaOrmConfig; + +#[test] +fn default_config_values() { + let cfg = SeaOrmConfig::default(); + assert_eq!(cfg.driver, "sqlite"); + assert_eq!(cfg.url, ""); + assert_eq!(cfg.max_connections, 10); + assert_eq!(cfg.min_connections, 1); + assert_eq!(cfg.connect_timeout_secs, 5); + assert_eq!(cfg.idle_timeout_secs, 300); + assert!(cfg.schema.is_none()); + assert!(cfg.services.is_empty()); +} + +#[test] +fn driver_constructors_and_builder() { + let pg = SeaOrmConfig::postgres("postgres://localhost/db"); + assert_eq!(pg.driver, "postgres"); + assert_eq!(pg.url, "postgres://localhost/db"); + + let my = SeaOrmConfig::mysql("mysql://localhost/db"); + assert_eq!(my.driver, "mysql"); + + let lite = SeaOrmConfig::sqlite("sqlite::memory:").max_connections(42); + assert_eq!(lite.driver, "sqlite"); + assert_eq!(lite.max_connections, 42); +} + +#[test] +fn serde_applies_defaults_for_missing_fields() { + let cfg: SeaOrmConfig = + serde_json::from_str(r#"{"url":"sqlite::memory:"}"#).expect("deserialize"); + assert_eq!(cfg.driver, "sqlite"); + assert_eq!(cfg.max_connections, 10); + assert_eq!(cfg.min_connections, 1); +} diff --git a/crates/vil_db_sqlx/tests/unit.rs b/crates/vil_db_sqlx/tests/unit.rs new file mode 100644 index 00000000..fca2e0db --- /dev/null +++ b/crates/vil_db_sqlx/tests/unit.rs @@ -0,0 +1,56 @@ +//! Pure unit tests for `vil_db_sqlx` — no live database required. +//! Run: `cargo test -p vil_db_sqlx` + +use vil_db_sqlx::SqlxConfig; + +#[test] +fn default_config_values() { + let cfg = SqlxConfig::default(); + assert_eq!(cfg.driver, "sqlite"); + assert_eq!(cfg.max_connections, 10); + assert_eq!(cfg.min_connections, 1); + assert_eq!(cfg.connect_timeout_secs, 5); + assert_eq!(cfg.idle_timeout_secs, 300); + assert_eq!(cfg.ssl_mode, "prefer"); + assert!(cfg.services.is_empty()); +} + +#[test] +fn driver_constructors_and_builders() { + let pg = SqlxConfig::postgres("postgres://localhost/db") + .max_connections(25) + .min_connections(3) + .timeout(9); + assert_eq!(pg.driver, "postgres"); + assert_eq!(pg.url, "postgres://localhost/db"); + assert_eq!(pg.max_connections, 25); + assert_eq!(pg.min_connections, 3); + assert_eq!(pg.connect_timeout_secs, 9); + + assert_eq!(SqlxConfig::mysql("mysql://x/db").driver, "mysql"); + assert_eq!(SqlxConfig::sqlite("sqlite::memory:").driver, "sqlite"); +} + +#[test] +fn is_for_service_matches_rules() { + let all = SqlxConfig::sqlite("sqlite::memory:"); + assert!(all.is_for_service("anything")); + + let mut wildcard = SqlxConfig::sqlite("sqlite::memory:"); + wildcard.services = vec!["*".to_string()]; + assert!(wildcard.is_for_service("orders")); + + let mut scoped = SqlxConfig::sqlite("sqlite::memory:"); + scoped.services = vec!["orders".to_string()]; + assert!(scoped.is_for_service("orders")); + assert!(!scoped.is_for_service("billing")); +} + +#[test] +fn serde_applies_defaults_for_missing_fields() { + let cfg: SqlxConfig = + serde_json::from_str(r#"{"url":"sqlite::memory:"}"#).expect("deserialize"); + assert_eq!(cfg.driver, "sqlite"); + assert_eq!(cfg.ssl_mode, "prefer"); + assert_eq!(cfg.max_connections, 10); +} diff --git a/crates/vil_db_timeseries/Cargo.toml b/crates/vil_db_timeseries/Cargo.toml index 5c472822..35ac5bb7 100644 --- a/crates/vil_db_timeseries/Cargo.toml +++ b/crates/vil_db_timeseries/Cargo.toml @@ -22,3 +22,6 @@ tokio = { workspace = true } serde = { workspace = true } influxdb2 = { version = "0.5", optional = true } futures = { version = "0.3", optional = true } + +[dev-dependencies] +serde_json = "1" diff --git a/crates/vil_db_timeseries/tests/unit.rs b/crates/vil_db_timeseries/tests/unit.rs new file mode 100644 index 00000000..3c3ec611 --- /dev/null +++ b/crates/vil_db_timeseries/tests/unit.rs @@ -0,0 +1,40 @@ +//! Pure unit tests for `vil_db_timeseries` — no live backend required. +//! Run: `cargo test -p vil_db_timeseries` + +use vil_db_timeseries::{TimeseriesConfig, TimeseriesFault}; + +#[test] +fn config_new_defaults() { + let cfg = TimeseriesConfig::new("http://localhost:8086", "myorg", "my-token", "metrics"); + assert_eq!(cfg.host, "http://localhost:8086"); + assert_eq!(cfg.org, "myorg"); + assert_eq!(cfg.token, "my-token"); + assert_eq!(cfg.bucket, "metrics"); + assert_eq!(cfg.pool_id, 0); +} + +#[test] +fn config_serde_round_trip() { + let cfg = TimeseriesConfig::new("http://h:8086", "o", "t", "b"); + let json = serde_json::to_string(&cfg).expect("serialize"); + let back: TimeseriesConfig = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(back.host, cfg.host); + assert_eq!(back.org, cfg.org); + assert_eq!(back.token, cfg.token); + assert_eq!(back.bucket, cfg.bucket); +} + +#[test] +fn fault_metadata_is_consistent() { + let conn = TimeseriesFault::ConnectionFailed { host_hash: 1, reason_code: 2 }; + assert_eq!(conn.kind(), "ConnectionFailed"); + assert!(conn.is_retryable()); + + let wr = TimeseriesFault::WriteFailed { bucket_hash: 3, reason_code: 4 }; + assert_eq!(wr.kind(), "WriteFailed"); + assert!(!wr.is_retryable()); + + let fne = TimeseriesFault::FeatureNotEnabled { feature_hash: 5 }; + assert_eq!(fne.kind(), "FeatureNotEnabled"); + assert!(!fne.is_retryable()); +}