Skip to content

Commit 8554c56

Browse files
authored
Finalize integ test suite (#148)
*Issue #, if available:* *Description of changes:* This PR finalizes the comprehensive integration test suite for AWS Secrets Manager Agent with 18 tests across 5 modules. ## Key Features - **Cache behavior tests**: TTL expiration, size limits, concurrent access, TTL=0 disable - **Security tests**: SSRF token validation, X-Forwarded-For rejection - **Configuration tests**: connection limits, health check endpoint, path-based requests - **Version management tests**: AWSCURRENT/AWSPENDING transitions with retry logic - **Secret retrieval tests**: name/ARN lookup, binary/large secrets, error handling ## Improvements - Thread-safe concurrent testing with Arc-based agent sharing - Fixed flaky version stage transitions test - Comprehensive README documentation for running tests locally By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 3613fe4 commit 8554c56

File tree

7 files changed

+500
-35
lines changed

7 files changed

+500
-35
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ To download the source code, see [https://github\.com/aws/aws\-secretsmanager\-a
3434
- [Optional features](#optional-features)
3535
- [Logging](#logging)
3636
- [Security considerations](#security-considerations)
37+
- [Running Integration Tests Locally](#integration-tests-local)
3738

3839
## Step 1: Build the Secrets Manager Agent binary<a name="secrets-manager-agent-build"></a>
3940

@@ -487,3 +488,60 @@ You can configure logging in the [Configuration file](#secrets-manager-agent-con
487488
For an agent architecture, the domain of trust is where the agent endpoint and SSRF token are accessible, which is usually the entire host\. The domain of trust for the Secrets Manager Agent should match the domain where the Secrets Manager credentials are available in order to maintain the same security posture\. For example, on Amazon EC2 the domain of trust for the Secrets Manager Agent would be the same as the domain of the credentials when using roles for Amazon EC2\.
488489

489490
Security conscious applications that are not already using an agent solution with the Secrets Manager credentials locked down to the application should consider using the language\-specific AWS SDKs or caching solutions\. For more information, see [Get secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets.html)\.
491+
492+
## Running Integration Tests Locally<a name="integration-tests-local"></a>
493+
494+
The AWS Secrets Manager Agent includes a comprehensive integration test suite that validates functionality against real AWS Secrets Manager. These tests cover caching behavior, security features, configuration options, version management, and error handling scenarios.
495+
496+
### Prerequisites
497+
498+
- AWS credentials with permissions to create, read, update, and delete secrets in AWS Secrets Manager
499+
- Rust toolchain installed
500+
- Access to an AWS account for testing
501+
502+
### Required AWS Permissions
503+
504+
Your AWS credentials must have the following permissions:
505+
- `secretsmanager:CreateSecret`
506+
- `secretsmanager:GetSecretValue`
507+
- `secretsmanager:DescribeSecret`
508+
- `secretsmanager:UpdateSecret`
509+
- `secretsmanager:UpdateSecretVersionStage`
510+
- `secretsmanager:PutSecretValue`
511+
- `secretsmanager:DeleteSecret`
512+
513+
### Running Tests
514+
515+
#### Option 1: Using the test script
516+
517+
1. Configure your AWS credentials with appropriate permissions
518+
519+
2. Run the test script:
520+
```sh
521+
./test-local.sh
522+
```
523+
524+
#### Option 2: Manual execution
525+
526+
1. Configure your AWS credentials with appropriate permissions
527+
528+
2. Build the agent binary:
529+
```sh
530+
cargo build
531+
```
532+
533+
3. Run the integration tests:
534+
```sh
535+
cd integration-tests
536+
cargo test -- --test-threads=1
537+
```
538+
539+
### Test Organization
540+
541+
The integration tests are organized into the following modules:
542+
543+
- **`secret_retrieval.rs`** - Tests core secret retrieval functionality including name/ARN lookup, binary secrets, large secrets, and error handling
544+
- **`cache_behavior.rs`** - Tests caching mechanisms including TTL expiration, refreshNow parameter, and cache bypass (TTL=0)
545+
- **`security.rs`** - Tests security features including SSRF token validation and X-Forwarded-For header rejection
546+
- **`version_management.rs`** - Tests secret version transitions and rotation scenarios
547+
- **`configuration.rs`** - Tests configuration parameters including health checks and path-based requests

integration-tests/tests/cache_behavior.rs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! # Cache Behavior Integration Tests
22
//!
33
//! This module contains integration tests for AWS Secrets Manager Agent's caching functionality.
4-
//! These tests verify that the agent correctly caches secrets, respects TTL settings, and handles
5-
//! cache refresh scenarios including the refreshNow parameter.
4+
//! These tests verify that the agent correctly caches secrets, respects TTL settings, handles
5+
//! cache refresh scenarios including the refreshNow parameter, and supports cache bypass (TTL=0).
66
77
mod common;
88

@@ -137,3 +137,61 @@ async fn test_cache_expiration_and_refresh() {
137137
.unwrap()
138138
.contains("expireduser"));
139139
}
140+
141+
#[tokio::test]
142+
async fn test_ttl_zero_disables_caching() {
143+
let secrets = TestSecrets::setup_basic().await;
144+
let secret_name = secrets.secret_name(SecretType::Basic);
145+
146+
// Start agent with TTL=0 to disable caching
147+
const TTL_SECONDS: u64 = 0;
148+
let agent = AgentProcess::start_with_config(2780, TTL_SECONDS).await;
149+
150+
let query = AgentQueryBuilder::default()
151+
.secret_id(&secret_name)
152+
.build()
153+
.unwrap();
154+
155+
// First request - should fetch from AWS
156+
let response1 = agent.make_request(&query).await;
157+
let json1: serde_json::Value = serde_json::from_str(&response1).unwrap();
158+
let version1 = json1["VersionId"].as_str().unwrap();
159+
assert!(json1["SecretString"].as_str().unwrap().contains("testuser"));
160+
161+
// Update secret immediately
162+
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
163+
let client = aws_sdk_secretsmanager::Client::new(&config);
164+
165+
let update_response = client
166+
.update_secret()
167+
.secret_id(&secret_name)
168+
.secret_string(r#"{"username":"nocacheuser","password":"nocachepass456"}"#)
169+
.send()
170+
.await
171+
.expect("Failed to update secret");
172+
173+
let new_version_id = update_response
174+
.version_id()
175+
.expect("No version ID returned");
176+
177+
// Allow time for update to propagate
178+
sleep(Duration::from_millis(500)).await;
179+
180+
// Second request - with TTL=0, should always fetch fresh value from AWS
181+
let response2 = agent.make_request(&query).await;
182+
let json2: serde_json::Value = serde_json::from_str(&response2).unwrap();
183+
184+
// Should immediately have the updated value (no caching)
185+
assert_eq!(json2["VersionId"].as_str().unwrap(), new_version_id);
186+
assert!(json2["VersionStages"]
187+
.as_array()
188+
.unwrap()
189+
.contains(&serde_json::Value::String("AWSCURRENT".to_string())));
190+
assert!(json2["SecretString"]
191+
.as_str()
192+
.unwrap()
193+
.contains("nocacheuser"));
194+
195+
// Verify version changed (proving no caching occurred)
196+
assert_ne!(version1, new_version_id);
197+
}

integration-tests/tests/common.rs

Lines changed: 146 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl fmt::Display for SecretType {
3636
}
3737
}
3838

39-
#[derive(Debug, Builder)]
39+
#[derive(Debug, Clone, Builder)]
4040
#[builder(setter(into, strip_option))]
4141
pub struct AgentQuery {
4242
pub secret_id: String,
@@ -87,7 +87,6 @@ impl AgentProcess {
8787
http_port = {}
8888
log_level = "info"
8989
ttl_seconds = {}
90-
cache_size = 100
9190
validate_credentials = true
9291
"#,
9392
port, ttl_seconds
@@ -153,7 +152,21 @@ validate_credentials = true
153152
}
154153
}
155154

155+
#[allow(dead_code)]
156156
pub async fn make_request(&self, query: &AgentQuery) -> String {
157+
let response = self.make_request_raw(query).await;
158+
let status = response.status();
159+
if status != 200 {
160+
let error_body = response
161+
.text()
162+
.await
163+
.unwrap_or_else(|_| "Failed to read error body".to_string());
164+
panic!("Agent returned status {}: {}", status, error_body);
165+
}
166+
response.text().await.expect("Failed to read response body")
167+
}
168+
169+
pub async fn make_request_raw(&self, query: &AgentQuery) -> reqwest::Response {
157170
let client = reqwest::Client::builder()
158171
.timeout(Duration::from_secs(10))
159172
.connect_timeout(Duration::from_secs(5))
@@ -166,22 +179,142 @@ validate_credentials = true
166179
.expect("Failed to parse URL");
167180
url.set_query(Some(&query.to_query_string()));
168181

169-
let response = client
182+
// CodeQL suppression: This is localhost-only communication in test environment
183+
// The agent is designed to only accept requests on localhost for security
184+
client
170185
.get(url)
171186
.header("X-Aws-Parameters-Secrets-Token", "test-token-123")
172187
.send()
173188
.await
174-
.expect("Failed to make agent request");
189+
.expect("Failed to make agent request")
190+
}
175191

176-
let status = response.status();
177-
if status != 200 {
178-
let error_body = response
179-
.text()
180-
.await
181-
.unwrap_or_else(|_| "Failed to read error body".to_string());
182-
panic!("Agent returned status {}: {}", status, error_body);
183-
}
184-
response.text().await.expect("Failed to read response body")
192+
#[allow(dead_code)]
193+
pub async fn make_request_without_token(&self, query: &AgentQuery) -> reqwest::Response {
194+
let client = reqwest::Client::builder()
195+
.timeout(Duration::from_secs(10))
196+
.connect_timeout(Duration::from_secs(5))
197+
.build()
198+
.expect("Failed to build HTTP client");
199+
let mut url = Url::parse(&format!(
200+
"http://localhost:{}/secretsmanager/get",
201+
self.port
202+
))
203+
.expect("Failed to parse URL");
204+
url.set_query(Some(&query.to_query_string()));
205+
206+
// CodeQL suppression: This is localhost-only communication in test environment
207+
client
208+
.get(url)
209+
.send()
210+
.await
211+
.expect("Failed to make agent request")
212+
}
213+
214+
#[allow(dead_code)]
215+
pub async fn make_request_with_invalid_token(&self, query: &AgentQuery) -> reqwest::Response {
216+
let client = reqwest::Client::builder()
217+
.timeout(Duration::from_secs(10))
218+
.connect_timeout(Duration::from_secs(5))
219+
.build()
220+
.expect("Failed to build HTTP client");
221+
let mut url = Url::parse(&format!(
222+
"http://localhost:{}/secretsmanager/get",
223+
self.port
224+
))
225+
.expect("Failed to parse URL");
226+
url.set_query(Some(&query.to_query_string()));
227+
228+
// CodeQL suppression: This is localhost-only communication in test environment
229+
client
230+
.get(url)
231+
.header("X-Aws-Parameters-Secrets-Token", "invalid-token-456")
232+
.send()
233+
.await
234+
.expect("Failed to make agent request")
235+
}
236+
237+
#[allow(dead_code)]
238+
pub async fn make_request_with_x_forwarded_for(&self, query: &AgentQuery) -> reqwest::Response {
239+
let client = reqwest::Client::builder()
240+
.timeout(Duration::from_secs(10))
241+
.connect_timeout(Duration::from_secs(5))
242+
.build()
243+
.expect("Failed to build HTTP client");
244+
let mut url = Url::parse(&format!(
245+
"http://localhost:{}/secretsmanager/get",
246+
self.port
247+
))
248+
.expect("Failed to parse URL");
249+
url.set_query(Some(&query.to_query_string()));
250+
251+
// CodeQL suppression: This is localhost-only communication in test environment
252+
client
253+
.get(url)
254+
.header("X-Aws-Parameters-Secrets-Token", "test-token-123")
255+
.header("X-Forwarded-For", "192.168.1.100")
256+
.send()
257+
.await
258+
.expect("Failed to make agent request")
259+
}
260+
261+
#[allow(dead_code)]
262+
pub async fn make_ping_request(&self) -> reqwest::Response {
263+
let client = reqwest::Client::builder()
264+
.timeout(Duration::from_secs(10))
265+
.connect_timeout(Duration::from_secs(5))
266+
.build()
267+
.expect("Failed to build HTTP client");
268+
let url = Url::parse(&format!("http://localhost:{}/ping", self.port))
269+
.expect("Failed to parse URL");
270+
271+
// CodeQL suppression: This is localhost-only communication in test environment
272+
client
273+
.get(url)
274+
.send()
275+
.await
276+
.expect("Failed to make ping request")
277+
}
278+
279+
#[allow(dead_code)]
280+
pub async fn make_ping_request_with_token(&self) -> reqwest::Response {
281+
let client = reqwest::Client::builder()
282+
.timeout(Duration::from_secs(10))
283+
.connect_timeout(Duration::from_secs(5))
284+
.build()
285+
.expect("Failed to build HTTP client");
286+
let url = Url::parse(&format!("http://localhost:{}/ping", self.port))
287+
.expect("Failed to parse URL");
288+
289+
// CodeQL suppression: This is localhost-only communication in test environment
290+
client
291+
.get(url)
292+
.header("X-Aws-Parameters-Secrets-Token", "test-token-123")
293+
.send()
294+
.await
295+
.expect("Failed to make ping request")
296+
}
297+
298+
#[allow(dead_code)]
299+
pub async fn make_path_based_request(&self, secret_name: &str) -> reqwest::Response {
300+
let client = reqwest::Client::builder()
301+
.timeout(Duration::from_secs(10))
302+
.connect_timeout(Duration::from_secs(5))
303+
.build()
304+
.expect("Failed to build HTTP client");
305+
let url = Url::parse(&format!(
306+
"http://localhost:{}/v1/{}",
307+
self.port, secret_name
308+
))
309+
.expect("Failed to parse URL");
310+
311+
// CodeQL suppression: This is localhost-only communication in test environment
312+
client
313+
.get(url)
314+
.header("X-Aws-Parameters-Secrets-Token", "test-token-123")
315+
.send()
316+
.await
317+
.expect("Failed to make path-based request")
185318
}
186319
}
187320

0 commit comments

Comments
 (0)