From 12161b7406c0f0874506a58d2c37fd727abddd3d Mon Sep 17 00:00:00 2001 From: C J Silverio Date: Sat, 28 Mar 2026 19:26:59 -0700 Subject: [PATCH] fix: make postgres tests hygienic for persistent local databases Each test now cleans up only the data it created instead of truncating the entire table. Tests use unique, prefixed names and clean up both before and after to handle interrupted prior runs. Store tests: - Removed DELETE FROM secrets in test_store() - Added cleanup() helper that deletes by specific names - list test uses contains() assertions instead of exact count - Unique names per test (pg-roundtrip, pg-list-a, etc.) Audit tests: - Removed DELETE FROM audit_events in test_audit_log() - Added cleanup_agent/cleanup_secret/cleanup_lease helpers - Unique agent names (test-alice, test-bob) to avoid collisions - Each test cleans before and after --- .justfile | 13 +-- Cargo.toml | 7 +- crates/zerolease-agent/Cargo.toml | 2 +- crates/zerolease-provider/Cargo.toml | 2 +- crates/zerolease-store-aws-sm/Cargo.toml | 2 +- crates/zerolease-store-postgres/Cargo.toml | 2 +- crates/zerolease-store-postgres/src/audit.rs | 93 +++++++++++++++----- crates/zerolease-store-postgres/src/store.rs | 88 ++++++++++++------ crates/zerolease-store-rusqlite/Cargo.toml | 2 +- fuzz/Cargo.toml | 2 +- 10 files changed, 148 insertions(+), 65 deletions(-) diff --git a/.justfile b/.justfile index 716dfd6..4dc9615 100644 --- a/.justfile +++ b/.justfile @@ -4,7 +4,7 @@ _help: # Run all unit tests using nextest. test: cargo nextest run --workspace --all-targets --future-incompat-report - cargo nextest run -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml --future-incompat-report + cargo nextest run -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml --run-ignored ignored-only --future-incompat-report # Run the fuzz tests against the wireline protocol. fuzz: @@ -27,8 +27,8 @@ fmt: ci: test fmt cargo clippy --workspace --all-targets cargo clippy -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml - cargo test --doc - cargo test --doc -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml + # cargo test --doc --no-tests + # cargo test --doc -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml --no-tests # Install required tools setup: @@ -40,11 +40,12 @@ setup: version BUMP: #!/usr/bin/env bash set -e - current=$(tomato get package.version Cargo.toml) + current=$(tomato get workspace.package.version Cargo.toml) version=$(semver-bump {{ BUMP }} "$current") - tomato set package.version "$version" Cargo.toml &> /dev/null + tomato set workspace.package.version "$version" Cargo.toml &> /dev/null + tomato set package.version "$version" crates/zerolease-store-postgres/Cargo.toml &> /dev/null cargo generate-lockfile - git commit Cargo.toml -m "v${version}" + git commit Cargo.{toml,lockfile} crates/zerolease-store-postgres/Cargo.{toml,lockfile} -m "v${version}" git tag "v${version}" echo "Release tagged for version v${version}" diff --git a/Cargo.toml b/Cargo.toml index 4769848..8789713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ exclude = ["crates/zerolease-store-postgres"] resolver = "3" [workspace.package] +version = "0.1.0" edition = "2024" authors = ["C J Silverio "] license = "Apache-2.0" @@ -37,10 +38,10 @@ zerolease = { path = ".", default-features = false } [package] name = "zerolease" +description = "A lightweight, agent-aware credential vault with lease-based access control" authors.workspace = true -version = "0.1.0" +version.workspace = true edition.workspace = true -description = "A lightweight, agent-aware credential vault with lease-based access control" license.workspace = true repository.workspace = true keywords = ["credentials", "vault", "security", "agents", "secrets"] @@ -58,7 +59,7 @@ chrono.workspace = true secrecy.workspace = true serde.workspace = true serde_json.workspace = true -sha2 = "0.10" +sha2 = "0.11" thiserror.workspace = true tokio.workspace = true tracing = "0.1" diff --git a/crates/zerolease-agent/Cargo.toml b/crates/zerolease-agent/Cargo.toml index 035f18c..91956fb 100644 --- a/crates/zerolease-agent/Cargo.toml +++ b/crates/zerolease-agent/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zerolease-agent" -version = "0.1.0" description = "VM-side agent: credential provisioner, lease-aware proxy, git credential helper" +version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true diff --git a/crates/zerolease-provider/Cargo.toml b/crates/zerolease-provider/Cargo.toml index b9c96fc..9907d93 100644 --- a/crates/zerolease-provider/Cargo.toml +++ b/crates/zerolease-provider/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zerolease-provider" -version = "0.1.0" description = "CredentialProvider trait for lease-based credential access in AI agent tools" +version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true diff --git a/crates/zerolease-store-aws-sm/Cargo.toml b/crates/zerolease-store-aws-sm/Cargo.toml index c1723fd..bbe581c 100644 --- a/crates/zerolease-store-aws-sm/Cargo.toml +++ b/crates/zerolease-store-aws-sm/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zerolease-store-aws-sm" -version = "0.1.0" description = "AWS Secrets Manager-backed SecretStore for zerolease" +version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true diff --git a/crates/zerolease-store-postgres/Cargo.toml b/crates/zerolease-store-postgres/Cargo.toml index bc0740c..7b50379 100644 --- a/crates/zerolease-store-postgres/Cargo.toml +++ b/crates/zerolease-store-postgres/Cargo.toml @@ -4,8 +4,8 @@ [package] name = "zerolease-store-postgres" -version = "0.1.0" description = "PostgreSQL-backed storage backends for zerolease (SecretStore + AuditLog)" +version = "0.1.0" edition = "2024" authors = ["C J Silverio "] license = "Apache-2.0" diff --git a/crates/zerolease-store-postgres/src/audit.rs b/crates/zerolease-store-postgres/src/audit.rs index 2248538..d4af76a 100644 --- a/crates/zerolease-store-postgres/src/audit.rs +++ b/crates/zerolease-store-postgres/src/audit.rs @@ -146,49 +146,91 @@ mod tests { } async fn test_audit_log() -> PostgresAuditLog { - let log = PostgresAuditLog::new(&test_url()) + PostgresAuditLog::new(&test_url()) .await - .expect("should create audit log"); - sqlx::query("DELETE FROM audit_events") - .execute(&log.pool) - .await - .expect("should clean test data"); - log + .expect("should create audit log") } fn make_entry(agent: &str, event: AuditEvent) -> AuditEntry { AuditEntry::new(event, AgentId::new(agent), &PeerIdentity::Anonymous, AuditOutcome::Success) } + /// Clean up audit events for a specific agent (each test cleans its own data). + async fn cleanup_agent(log: &PostgresAuditLog, agent: &str) { + sqlx::query("DELETE FROM audit_events WHERE agent = $1") + .bind(agent) + .execute(&log.pool) + .await + .expect("should clean up agent events"); + } + + /// Clean up audit events for a specific secret name. + async fn cleanup_secret(log: &PostgresAuditLog, secret: &str) { + sqlx::query("DELETE FROM audit_events WHERE secret_name = $1") + .bind(secret) + .execute(&log.pool) + .await + .expect("should clean up secret events"); + } + + /// Clean up audit events for a specific lease. + async fn cleanup_lease(log: &PostgresAuditLog, lease: &LeaseId) { + sqlx::query("DELETE FROM audit_events WHERE lease_id = $1") + .bind(lease.as_uuid().to_string()) + .execute(&log.pool) + .await + .expect("should clean up lease events"); + } + #[tokio::test] #[ignore] // requires running PostgreSQL with zerolease_test database async fn record_and_query_by_agent() { let log = test_audit_log().await; + // Use unique agent names to avoid collisions with other tests. + cleanup_agent(&log, "test-alice").await; + cleanup_agent(&log, "test-bob").await; + for _ in 0..3 { - log.record(make_entry("alice", AuditEvent::DekRotated)).await.expect("record"); + log.record(make_entry("test-alice", AuditEvent::DekRotated)) + .await + .expect("record should succeed"); } - log.record(make_entry("bob", AuditEvent::DekRotated)).await.expect("record"); + log.record(make_entry("test-bob", AuditEvent::DekRotated)) + .await + .expect("record should succeed"); + + let results = log.query_by_agent(&AgentId::new("test-alice"), 10).await.expect("query"); + assert_eq!(results.len(), 3, "alice should have 3 events"); - let results = log.query_by_agent(&AgentId::new("alice"), 10).await.expect("query"); - assert_eq!(results.len(), 3); + let bob = log.query_by_agent(&AgentId::new("test-bob"), 10).await.expect("query"); + assert_eq!(bob.len(), 1, "bob should have 1 event"); - let bob = log.query_by_agent(&AgentId::new("bob"), 10).await.expect("query"); - assert_eq!(bob.len(), 1); + cleanup_agent(&log, "test-alice").await; + cleanup_agent(&log, "test-bob").await; } #[tokio::test] #[ignore] async fn query_by_secret() { let log = test_audit_log().await; - log.record(make_entry("agent", AuditEvent::LeaseGranted { + cleanup_secret(&log, "pg-audit-secret-a").await; + + log.record(make_entry("audit-agent", AuditEvent::LeaseGranted { lease_id: LeaseId::new(), - secret_name: SecretName::new("pg-secret-a"), + secret_name: SecretName::new("pg-audit-secret-a"), domains: vec![DomainScope::new("api.example.com")], ttl_seconds: 900, - })).await.expect("record"); + })) + .await + .expect("record should succeed"); + + let results = log + .query_by_secret(&SecretName::new("pg-audit-secret-a"), 10) + .await + .expect("query should succeed"); + assert_eq!(results.len(), 1, "should find 1 event for the secret"); - let results = log.query_by_secret(&SecretName::new("pg-secret-a"), 10).await.expect("query"); - assert_eq!(results.len(), 1); + cleanup_secret(&log, "pg-audit-secret-a").await; } #[tokio::test] @@ -196,14 +238,19 @@ mod tests { async fn query_by_lease() { let log = test_audit_log().await; let lease = LeaseId::new(); - log.record(make_entry("agent", AuditEvent::LeaseGranted { + + log.record(make_entry("audit-agent", AuditEvent::LeaseGranted { lease_id: lease, - secret_name: SecretName::new("pg-secret"), + secret_name: SecretName::new("pg-audit-lease-secret"), domains: vec![DomainScope::new("example.com")], ttl_seconds: 900, - })).await.expect("record"); + })) + .await + .expect("record should succeed"); + + let results = log.query_by_lease(&lease).await.expect("query should succeed"); + assert_eq!(results.len(), 1, "should find 1 event for the lease"); - let results = log.query_by_lease(&lease).await.expect("query"); - assert_eq!(results.len(), 1); + cleanup_lease(&log, &lease).await; } } diff --git a/crates/zerolease-store-postgres/src/store.rs b/crates/zerolease-store-postgres/src/store.rs index 31b43e0..4633352 100644 --- a/crates/zerolease-store-postgres/src/store.rs +++ b/crates/zerolease-store-postgres/src/store.rs @@ -247,14 +247,9 @@ mod tests { } async fn test_store() -> PostgresStore { - let store = PostgresStore::new(&test_url()) + PostgresStore::new(&test_url()) .await - .expect("should create store"); - sqlx::query("DELETE FROM secrets") - .execute(&store.pool) - .await - .expect("should clean test data"); - store + .expect("should create store") } fn test_params(name: &str) -> StoreSecretParams { @@ -268,62 +263,101 @@ mod tests { } } + /// Clean up specific secrets by name (each test cleans up after itself). + async fn cleanup(store: &PostgresStore, names: &[&str]) { + for name in names { + let _ = store.delete(&SecretName::new(*name)).await; + } + } + #[tokio::test] #[ignore] // requires running PostgreSQL with zerolease_test database async fn put_and_get_round_trip() { let store = test_store().await; - let stored = store.put(test_params("pg-secret")).await.expect("put"); - assert_eq!(stored.name, SecretName::new("pg-secret")); - assert_eq!(stored.version, 1); - let fetched = store.get(&SecretName::new("pg-secret")).await.expect("get"); - assert_eq!(fetched.ciphertext, stored.ciphertext); + cleanup(&store, &["pg-roundtrip"]).await; + + let stored = store.put(test_params("pg-roundtrip")).await.expect("put should store secret"); + assert_eq!(stored.name, SecretName::new("pg-roundtrip"), "name should match"); + assert_eq!(stored.version, 1, "initial version should be 1"); + + let fetched = store.get(&SecretName::new("pg-roundtrip")).await.expect("get should retrieve secret"); + assert_eq!(fetched.ciphertext, stored.ciphertext, "ciphertext should match"); + + cleanup(&store, &["pg-roundtrip"]).await; } #[tokio::test] #[ignore] async fn put_duplicate_name_errors() { let store = test_store().await; - store.put(test_params("pg-dup")).await.expect("first put"); - let err = store.put(test_params("pg-dup")).await.expect_err("dup").to_string(); - assert!(err.contains("already exists"), "error was: {err}"); + cleanup(&store, &["pg-dup"]).await; + + store.put(test_params("pg-dup")).await.expect("first put should succeed"); + let err = store.put(test_params("pg-dup")).await.expect_err("duplicate put should fail").to_string(); + assert!(err.contains("already exists"), "expected 'already exists', got: {err}"); + + cleanup(&store, &["pg-dup"]).await; } #[tokio::test] #[ignore] async fn get_missing_errors() { let store = test_store().await; - let err = store.get(&SecretName::new("pg-nope")).await.expect_err("missing").to_string(); - assert!(err.contains("not found"), "error was: {err}"); + + let err = store + .get(&SecretName::new("pg-nonexistent")) + .await + .expect_err("get for missing secret should fail") + .to_string(); + assert!(err.contains("not found"), "expected 'not found', got: {err}"); } #[tokio::test] #[ignore] async fn update_increments_version() { let store = test_store().await; - store.put(test_params("pg-versioned")).await.expect("put"); + cleanup(&store, &["pg-versioned"]).await; + + store.put(test_params("pg-versioned")).await.expect("put should create secret"); let updated = store .update(&SecretName::new("pg-versioned"), vec![10, 20], vec![1; 12], CipherAlgorithm::Aes256Gcm) .await - .expect("update"); - assert_eq!(updated.version, 2); + .expect("update should succeed"); + assert_eq!(updated.version, 2, "version should increment to 2"); + + cleanup(&store, &["pg-versioned"]).await; } #[tokio::test] #[ignore] async fn delete_removes_secret() { let store = test_store().await; - store.put(test_params("pg-doomed")).await.expect("put"); - store.delete(&SecretName::new("pg-doomed")).await.expect("delete"); - assert!(store.get(&SecretName::new("pg-doomed")).await.is_err()); + cleanup(&store, &["pg-doomed"]).await; + + store.put(test_params("pg-doomed")).await.expect("put should create secret"); + store.delete(&SecretName::new("pg-doomed")).await.expect("delete should succeed"); + + let err = store + .get(&SecretName::new("pg-doomed")) + .await + .expect_err("get after delete should fail"); + assert!(err.to_string().contains("not found"), "expected 'not found' after delete"); } #[tokio::test] #[ignore] async fn list_returns_metadata() { let store = test_store().await; - store.put(test_params("pg-first")).await.expect("put"); - store.put(test_params("pg-second")).await.expect("put"); - let list = store.list().await.expect("list"); - assert_eq!(list.len(), 2); + cleanup(&store, &["pg-list-a", "pg-list-b"]).await; + + store.put(test_params("pg-list-a")).await.expect("put first"); + store.put(test_params("pg-list-b")).await.expect("put second"); + + let list = store.list().await.expect("list should succeed"); + let names: Vec = list.iter().map(|m| m.name.as_str().to_string()).collect(); + assert!(names.contains(&"pg-list-a".to_string()), "list should contain pg-list-a, got: {names:?}"); + assert!(names.contains(&"pg-list-b".to_string()), "list should contain pg-list-b, got: {names:?}"); + + cleanup(&store, &["pg-list-a", "pg-list-b"]).await; } } diff --git a/crates/zerolease-store-rusqlite/Cargo.toml b/crates/zerolease-store-rusqlite/Cargo.toml index 83ce0a4..1c21af5 100644 --- a/crates/zerolease-store-rusqlite/Cargo.toml +++ b/crates/zerolease-store-rusqlite/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zerolease-store-rusqlite" -version = "0.1.0" description = "Rusqlite-backed storage backends for zerolease (SecretStore + AuditLog)" +version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 73d9c5f..aa40d42 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerolease-fuzz" -version = "0.0.0" +version = "0.1.0" publish = false edition = "2024"