Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ jobs:

# Run if:
# 1. Manual trigger or push to main, OR
# 2. PR from trusted author (COLLABORATOR), OR
# 3. PR with "safe to test" label
# 2. PR with "safe-to-test" label
if: |
github.event_name != 'pull_request_target' ||
github.event.pull_request.author_association == 'COLLABORATOR' ||
contains(github.event.pull_request.labels.*.name, 'safe-to-test')

permissions:
Expand Down
144 changes: 144 additions & 0 deletions integration-tests/tests/cache_behavior.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
mod common;

use common::*;
use std::time::Duration;
use tokio::time::sleep;

#[tokio::test]
async fn test_refresh_now_on_updated_secret_succeeds() {
let secrets = TestSecrets::setup().await;
let secret_name = secrets.secret_name(SecretType::Basic);

let agent = AgentProcess::start().await;

// First request - populate cache with original value
let query = AgentQueryBuilder::default()
.secret_id(&secret_name)
.build()
.unwrap();
let response1 = agent.make_request(&query).await;
let json1: serde_json::Value = serde_json::from_str(&response1).unwrap();
let original_secret = json1["SecretString"].as_str().unwrap();
assert!(original_secret.contains("testuser"));

// Update the secret in AWS (simulating manual update, not automatic rotation)
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let client = aws_sdk_secretsmanager::Client::new(&config);

let updated_secret_value = r#"{"username":"rotateduser","password":"rotatedpass123"}"#;
let update_response = client
.update_secret()
.secret_id(&secret_name)
.secret_string(updated_secret_value)
.send()
.await
.expect("Failed to update secret");

let new_version_id = update_response
.version_id()
.expect("No version ID returned");

// Second request without refreshNow - should return stale cached value
let response2 = agent.make_request(&query).await;
let json2: serde_json::Value = serde_json::from_str(&response2).unwrap();
let cached_secret = json2["SecretString"].as_str().unwrap();

// Should still have the old value from cache
assert!(cached_secret.contains("testuser"));
assert!(!cached_secret.contains("rotateduser"));

// Third request with refreshNow=true - should get fresh value
let refresh_query = AgentQueryBuilder::default()
.secret_id(&secret_name)
.refresh_now(true)
.build()
.unwrap();
let response3 = agent.make_request(&refresh_query).await;
let json3: serde_json::Value = serde_json::from_str(&response3).unwrap();
let fresh_secret = json3["SecretString"].as_str().unwrap();

// Should now have the updated value with new version ID and AWSCURRENT label
assert_eq!(json3["VersionId"].as_str().unwrap(), new_version_id);
assert!(json3["VersionStages"]
.as_array()
.unwrap()
.contains(&serde_json::Value::String("AWSCURRENT".to_string())));
assert!(fresh_secret.contains("rotateduser"));
assert!(!fresh_secret.contains("testuser"));
}

#[tokio::test]
async fn test_cache_expiration_and_refresh() {
let secrets = TestSecrets::setup().await;
let secret_name = secrets.secret_name(SecretType::Basic);

// Start agent with short TTL for faster testing
const TTL_SECONDS: u16 = 5;
let agent = AgentProcess::start_with_config(2777, TTL_SECONDS).await;

let query = AgentQueryBuilder::default()
.secret_id(&secret_name)
.build()
.unwrap();

// First request - populate cache
let response1 = agent.make_request(&query).await;
let json1: serde_json::Value = serde_json::from_str(&response1).unwrap();
let version1 = json1["VersionId"].as_str().unwrap();
assert!(json1["SecretString"].as_str().unwrap().contains("testuser"));

// Second request immediately - should hit cache (same version)
let response2 = agent.make_request(&query).await;
let json2: serde_json::Value = serde_json::from_str(&response2).unwrap();
assert_eq!(json1["VersionId"], json2["VersionId"]);

// Update secret while cache is still valid
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let client = aws_sdk_secretsmanager::Client::new(&config);

let update_response = client
.update_secret()
.secret_id(&secret_name)
.secret_string(r#"{"username":"expireduser","password":"expiredpass789"}"#)
.send()
.await
.expect("Failed to update secret");

let new_version_id = update_response
.version_id()
.expect("No version ID returned");

// Third request before TTL expires - should still return cached value
let response3 = agent.make_request(&query).await;
let json3: serde_json::Value = serde_json::from_str(&response3).unwrap();
assert_eq!(json3["VersionId"], version1); // Same version as cached
assert!(json3["SecretString"].as_str().unwrap().contains("testuser"));

// Wait for TTL to expire (TTL + buffer to ensure expiry)
sleep(Duration::from_secs(TTL_SECONDS as u64 + 1)).await;

// Fourth request after TTL expiry - should fetch fresh value from AWS
let response4 = agent.make_request(&query).await;
let json4: serde_json::Value = serde_json::from_str(&response4).unwrap();

// Should now have the updated value with new version ID and AWSCURRENT label
assert_eq!(json4["VersionId"].as_str().unwrap(), new_version_id);
assert!(json4["VersionStages"]
.as_array()
.unwrap()
.contains(&serde_json::Value::String("AWSCURRENT".to_string())));
assert!(json4["SecretString"]
.as_str()
.unwrap()
.contains("expireduser"));
assert!(!json4["SecretString"].as_str().unwrap().contains("testuser"));

// Fifth request immediately after - should use newly cached value
let response5 = agent.make_request(&query).await;
let json5: serde_json::Value = serde_json::from_str(&response5).unwrap();
assert_eq!(json4["VersionId"], json5["VersionId"]); // Same as previous
assert!(json5["SecretString"]
.as_str()
.unwrap()
.contains("expireduser"));
}
111 changes: 111 additions & 0 deletions integration-tests/tests/version_management.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
mod common;

use common::*;
use std::time::Duration;
use tokio::time::sleep;

#[tokio::test]
async fn test_version_stage_transitions() {
let secrets = TestSecrets::setup().await;
let secret_name = secrets.secret_name(SecretType::Versioned);

let agent = AgentProcess::start().await;

// Wait for AWSPENDING version to be available
let _ = secrets
.wait_for_pending_version(SecretType::Versioned)
.await;

// Get the version IDs for both stages
let (current_version_id, pending_version_id) =
secrets.get_version_ids(SecretType::Versioned).await;

// Test AWSPENDING stage before promotion
let pending_query = AgentQueryBuilder::default()
.secret_id(&secret_name)
.version_stage("AWSPENDING")
.build()
.unwrap();
let pending_response = agent.make_request(&pending_query).await;
let pending_json: serde_json::Value = serde_json::from_str(&pending_response).unwrap();

assert_eq!(pending_json["VersionId"], pending_version_id);
assert!(pending_json["SecretString"]
.as_str()
.unwrap()
.contains("pendinguser"));

// Test AWSCURRENT stage before promotion
let current_query = AgentQueryBuilder::default()
.secret_id(&secret_name)
.version_stage("AWSCURRENT")
.build()
.unwrap();
let current_response = agent.make_request(&current_query).await;
let current_json: serde_json::Value = serde_json::from_str(&current_response).unwrap();

assert_eq!(current_json["VersionId"], current_version_id);
assert!(current_json["SecretString"]
.as_str()
.unwrap()
.contains("currentuser"));

// Promote AWSPENDING to AWSCURRENT using update_secret_version_stage
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let client = aws_sdk_secretsmanager::Client::new(&config);

client
.update_secret_version_stage()
.secret_id(&secret_name)
.version_stage("AWSCURRENT")
.move_to_version_id(&pending_version_id)
.remove_from_version_id(&current_version_id)
.send()
.await
.expect("Failed to promote version stage");

// Wait for the promotion to propagate
sleep(Duration::from_secs(3)).await;

// Test that AWSCURRENT now points to the previously pending version (with refreshNow)
let promoted_query = AgentQueryBuilder::default()
.secret_id(&secret_name)
.version_stage("AWSCURRENT")
.refresh_now(true)
.build()
.unwrap();
let promoted_response = agent.make_request(&promoted_query).await;
let promoted_json: serde_json::Value = serde_json::from_str(&promoted_response).unwrap();

// After promotion, AWSCURRENT should now have the pending version ID and content
assert_eq!(promoted_json["VersionId"], pending_version_id);
assert!(promoted_json["SecretString"]
.as_str()
.unwrap()
.contains("pendinguser"));
assert!(promoted_json["VersionStages"]
.as_array()
.unwrap()
.contains(&serde_json::Value::String("AWSCURRENT".to_string())));

// Verify the old current version is no longer AWSCURRENT
let old_current_query = AgentQueryBuilder::default()
.secret_id(&secret_name)
.version_id(&current_version_id)
.refresh_now(true)
.build()
.unwrap();
let old_current_response = agent.make_request(&old_current_query).await;
let old_current_json: serde_json::Value = serde_json::from_str(&old_current_response).unwrap();

// The old version should still exist but not have AWSCURRENT stage
assert_eq!(old_current_json["VersionId"], current_version_id);
assert!(old_current_json["SecretString"]
.as_str()
.unwrap()
.contains("currentuser"));
assert!(!old_current_json["VersionStages"]
.as_array()
.unwrap()
.contains(&serde_json::Value::String("AWSCURRENT".to_string())));
}
Loading