Files
windmill/backend/windmill-common/tests/aws_sm_integration.rs
Ruben Fiszel 09bbc18bb7 feat: add AWS Secrets Manager as secret storage backend (Beta) (#8734)
* feat: add AWS KMS as secret backend (EE)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: switch from AWS KMS to AWS Secrets Manager as secret backend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add AWS Secrets Manager integration tests (requires LocalStack)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: mark AWS Secrets Manager as beta

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove leftover KMS handler functions from api-settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to include AWS Secrets Manager EE impl

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use full commit hash in ee-repo-ref.txt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* sqlx

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:17:15 +00:00

265 lines
8.4 KiB
Rust

//! Integration tests for AWS Secrets Manager secret backend.
//!
//! These tests require a running LocalStack instance with the `secretsmanager` service.
//!
//! ## Setup
//!
//! 1. Start LocalStack:
//!
//! ```bash
//! docker run -d --name localstack -p 4566:4566 \
//! -e SERVICES=secretsmanager \
//! localstack/localstack:3.8
//! ```
//!
//! 2. Run the tests:
//!
//! ```bash
//! RUN_AWS_SM_TESTS=1 cargo test -p windmill-common --features private,enterprise \
//! aws_sm_integration -- --nocapture
//! ```
//!
//! ## Environment variables
//!
//! - `RUN_AWS_SM_TESTS=1` - Required to run the tests
//! - `AWS_SM_ENDPOINT` - LocalStack endpoint (default: http://localhost:4566)
//! - `AWS_SM_REGION` - AWS region (default: us-east-1)
#[cfg(all(feature = "private", feature = "enterprise"))]
mod tests {
use windmill_common::secret_backend::{
test_aws_sm_connection, AwsSecretsManagerBackend, AwsSecretsManagerSettings,
SecretBackend,
};
fn should_run_aws_sm_tests() -> bool {
std::env::var("RUN_AWS_SM_TESTS")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
macro_rules! skip_if_no_aws_sm {
() => {
if !should_run_aws_sm_tests() {
println!("Skipping test: RUN_AWS_SM_TESTS=1 not set");
println!("To run: RUN_AWS_SM_TESTS=1 cargo test -p windmill-common --features private,enterprise aws_sm_integration -- --nocapture");
println!("Requires: docker run -d -p 4566:4566 -e SERVICES=secretsmanager localstack/localstack:3.8");
return;
}
};
}
fn test_settings() -> AwsSecretsManagerSettings {
AwsSecretsManagerSettings {
region: std::env::var("AWS_SM_REGION").unwrap_or_else(|_| "us-east-1".to_string()),
access_key_id: Some("test".to_string()),
secret_access_key: Some("test".to_string()),
endpoint_url: Some(
std::env::var("AWS_SM_ENDPOINT")
.unwrap_or_else(|_| "http://localhost:4566".to_string()),
),
prefix: Some("windmill-test/".to_string()),
}
}
#[tokio::test]
async fn test_connection() {
skip_if_no_aws_sm!();
let result = test_aws_sm_connection(&test_settings()).await;
assert!(result.is_ok(), "Connection test failed: {:?}", result.err());
println!(" ✓ Connection test passed");
}
#[tokio::test]
async fn test_create_and_get_secret() {
skip_if_no_aws_sm!();
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
.await
.unwrap();
backend
.set_secret("test-ws", "u/admin/my_secret", "super-secret-value")
.await
.unwrap();
let value = backend.get_secret("test-ws", "u/admin/my_secret").await.unwrap();
assert_eq!(value, "super-secret-value");
backend.delete_secret("test-ws", "u/admin/my_secret").await.unwrap();
println!(" ✓ Create + Get + Delete roundtrip passed");
}
#[tokio::test]
async fn test_update_secret() {
skip_if_no_aws_sm!();
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
.await
.unwrap();
backend
.set_secret("test-ws", "u/admin/update_test", "original")
.await
.unwrap();
backend
.set_secret("test-ws", "u/admin/update_test", "updated-value")
.await
.unwrap();
let value = backend
.get_secret("test-ws", "u/admin/update_test")
.await
.unwrap();
assert_eq!(value, "updated-value");
backend
.delete_secret("test-ws", "u/admin/update_test")
.await
.unwrap();
println!(" ✓ Update secret passed");
}
#[tokio::test]
async fn test_delete_and_verify_gone() {
skip_if_no_aws_sm!();
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
.await
.unwrap();
backend
.set_secret("test-ws", "u/admin/delete_test", "to-be-deleted")
.await
.unwrap();
backend
.delete_secret("test-ws", "u/admin/delete_test")
.await
.unwrap();
let result = backend.get_secret("test-ws", "u/admin/delete_test").await;
assert!(
matches!(result, Err(windmill_common::error::Error::NotFound(_))),
"Expected NotFound after delete, got: {:?}",
result
);
println!(" ✓ Delete + verify gone passed");
}
#[tokio::test]
async fn test_delete_nonexistent_is_ok() {
skip_if_no_aws_sm!();
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
.await
.unwrap();
let result = backend
.delete_secret("test-ws", "u/admin/never_existed")
.await;
assert!(
result.is_ok(),
"Delete of nonexistent should be Ok, got: {:?}",
result.err()
);
println!(" ✓ Delete nonexistent is Ok");
}
#[tokio::test]
async fn test_get_nonexistent_returns_not_found() {
skip_if_no_aws_sm!();
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
.await
.unwrap();
let result = backend
.get_secret("test-ws", "u/admin/does_not_exist")
.await;
assert!(
matches!(result, Err(windmill_common::error::Error::NotFound(_))),
"Expected NotFound, got: {:?}",
result
);
println!(" ✓ Get nonexistent returns NotFound");
}
#[tokio::test]
async fn test_unicode_and_special_chars() {
skip_if_no_aws_sm!();
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
.await
.unwrap();
let secret_value = "p@$$w0rd with unicode: \u{1F512}\u{1F511} and <xml>&\"quotes\"";
backend
.set_secret("test-ws", "u/admin/unicode_test", secret_value)
.await
.unwrap();
let retrieved = backend
.get_secret("test-ws", "u/admin/unicode_test")
.await
.unwrap();
assert_eq!(retrieved, secret_value);
backend
.delete_secret("test-ws", "u/admin/unicode_test")
.await
.unwrap();
println!(" ✓ Unicode and special chars passed");
}
#[tokio::test]
async fn test_workspace_isolation() {
skip_if_no_aws_sm!();
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
.await
.unwrap();
// Create secrets in different workspaces with the same path
backend
.set_secret("workspace-a", "u/admin/shared_name", "value-a")
.await
.unwrap();
backend
.set_secret("workspace-b", "u/admin/shared_name", "value-b")
.await
.unwrap();
// Each workspace should see its own value
let a = backend
.get_secret("workspace-a", "u/admin/shared_name")
.await
.unwrap();
let b = backend
.get_secret("workspace-b", "u/admin/shared_name")
.await
.unwrap();
assert_eq!(a, "value-a");
assert_eq!(b, "value-b");
// Cleanup
backend
.delete_secret("workspace-a", "u/admin/shared_name")
.await
.unwrap();
backend
.delete_secret("workspace-b", "u/admin/shared_name")
.await
.unwrap();
println!(" ✓ Workspace isolation passed");
}
#[tokio::test]
async fn test_backend_name() {
skip_if_no_aws_sm!();
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
.await
.unwrap();
assert_eq!(backend.backend_name(), "aws_secrets_manager");
println!(" ✓ Backend name is correct");
}
}
#[cfg(not(all(feature = "private", feature = "enterprise")))]
mod tests {
#[test]
fn test_aws_sm_requires_enterprise() {
println!("AWS Secrets Manager integration tests require Enterprise Edition features");
println!("Run with: cargo test -p windmill-common --features private,enterprise");
}
}