Skip to content

Commit 3f073a4

Browse files
committed
Refactor integration tests: categorize and secure workflow
- Updated caching.rs to only include true integration tests: - test_cache_after_secret_update: Real AWS secret rotation + cache staleness - test_real_ttl_expiration_timing: Real time-based TTL with actual delays - Removed performance-focused tests (moved to future performance suite) - Removed parameter behavior tests (moved to future unit tests) - Fixed GitHub Actions security vulnerability: - Changed pull_request_target to only trigger on 'labeled' events - Eliminates race condition where unapproved code could execute with AWS credentials - Each commit now requires explicit human approval via safe-to-test label - Auto-removes label after use to prevent persistent approval Integration tests now focus on real AWS interactions and timing behavior that cannot be effectively mocked or measured in unit tests.
1 parent b63c4fc commit 3f073a4

File tree

2 files changed

+14
-169
lines changed

2 files changed

+14
-169
lines changed

.github/workflows/integration-tests.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
push:
66
branches: ["main"]
77
pull_request_target:
8-
types: [opened, synchronize, reopened, ready_for_review, labeled]
8+
types: [labeled]
99

1010
env:
1111
CARGO_TERM_COLOR: always
@@ -16,18 +16,24 @@ jobs:
1616

1717
# Run if:
1818
# 1. Manual trigger or push to main, OR
19-
# 2. PR from trusted author (COLLABORATOR), OR
20-
# 3. PR from untrusted author with "safe to test" label
19+
# 2. PR with "safe-to-test" label added
2120
if: |
2221
github.event_name != 'pull_request_target' ||
23-
github.event.pull_request.author_association == 'COLLABORATOR' ||
2422
contains(github.event.pull_request.labels.*.name, 'safe-to-test')
2523
2624
permissions:
2725
id-token: write
2826
contents: read
27+
pull-requests: write
2928

3029
steps:
30+
- name: Remove safe-to-test label after use
31+
if: github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test')
32+
run: |
33+
gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/safe-to-test -X DELETE || true
34+
env:
35+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
3137
- name: Checkout
3238
uses: actions/checkout@v5
3339
with:

integration-tests/tests/caching.rs

Lines changed: 4 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,7 @@
11
mod common;
22

33
use common::*;
4-
use std::time::{Duration, Instant};
5-
6-
#[tokio::test]
7-
async fn test_cache_hit_behavior() {
8-
let secrets = TestSecrets::setup().await;
9-
let secret_name = secrets.secret_name(SecretType::Basic);
10-
11-
let agent = AgentProcess::start().await;
12-
13-
// First request - should fetch from AWS and cache
14-
let query = AgentQueryBuilder::default()
15-
.secret_id(&secret_name)
16-
.build()
17-
.unwrap();
18-
19-
let start_time = Instant::now();
20-
let response1 = agent.make_request(&query).await;
21-
let first_request_duration = start_time.elapsed();
22-
23-
let json1: serde_json::Value = serde_json::from_str(&response1).unwrap();
24-
assert_eq!(json1["Name"], secret_name);
25-
assert!(json1["SecretString"].as_str().unwrap().contains("testuser"));
26-
27-
// Second request - should be served from cache (much faster)
28-
let start_time = Instant::now();
29-
let response2 = agent.make_request(&query).await;
30-
let second_request_duration = start_time.elapsed();
31-
32-
let json2: serde_json::Value = serde_json::from_str(&response2).unwrap();
33-
assert_eq!(json2["Name"], secret_name);
34-
assert!(json2["SecretString"].as_str().unwrap().contains("testuser"));
35-
36-
// Verify responses are identical (from cache)
37-
assert_eq!(json1["VersionId"], json2["VersionId"]);
38-
assert_eq!(json1["SecretString"], json2["SecretString"]);
39-
40-
// Cache hit should be significantly faster than initial AWS call
41-
// Allow some tolerance for timing variations
42-
assert!(
43-
second_request_duration < first_request_duration / 2,
44-
"Cache hit should be faster. First: {:?}, Second: {:?}",
45-
first_request_duration,
46-
second_request_duration
47-
);
48-
}
49-
50-
#[tokio::test]
51-
async fn test_refresh_now_bypasses_cache() {
52-
let secrets = TestSecrets::setup().await;
53-
let secret_name = secrets.secret_name(SecretType::Basic);
54-
55-
let agent = AgentProcess::start().await;
56-
57-
// First request - populate cache
58-
let query = AgentQueryBuilder::default()
59-
.secret_id(&secret_name)
60-
.build()
61-
.unwrap();
62-
let response1 = agent.make_request(&query).await;
63-
let _json1: serde_json::Value = serde_json::from_str(&response1).unwrap();
64-
65-
// Second request with refreshNow=true - should bypass cache
66-
let refresh_query = AgentQueryBuilder::default()
67-
.secret_id(&secret_name)
68-
.refresh_now(true)
69-
.build()
70-
.unwrap();
71-
72-
let start_time = Instant::now();
73-
let response2 = agent.make_request(&refresh_query).await;
74-
let refresh_duration = start_time.elapsed();
75-
76-
let json2: serde_json::Value = serde_json::from_str(&response2).unwrap();
77-
78-
// Verify we got a valid response
79-
assert_eq!(json2["Name"], secret_name);
80-
assert!(json2["SecretString"].as_str().unwrap().contains("testuser"));
81-
82-
// refreshNow should take longer than a cache hit (it goes to AWS)
83-
// This is a network call, so should be measurably slower than cache
84-
assert!(
85-
refresh_duration > Duration::from_millis(10),
86-
"refreshNow should make AWS call, duration: {:?}",
87-
refresh_duration
88-
);
89-
90-
// Third request without refreshNow - should use updated cache
91-
let response3 = agent.make_request(&query).await;
92-
let json3: serde_json::Value = serde_json::from_str(&response3).unwrap();
93-
94-
assert_eq!(json3["Name"], secret_name);
95-
assert!(json3["SecretString"].as_str().unwrap().contains("testuser"));
96-
}
4+
use std::time::Duration;
975

986
#[tokio::test]
997
async fn test_cache_after_secret_update() {
@@ -175,91 +83,22 @@ async fn test_real_ttl_expiration_timing() {
17583
.unwrap();
17684

17785
// First request - populate cache
178-
let start_time = Instant::now();
17986
let response1 = agent.make_request(&query).await;
180-
let first_duration = start_time.elapsed();
18187
let json1: serde_json::Value = serde_json::from_str(&response1).unwrap();
18288
assert!(json1["SecretString"].as_str().unwrap().contains("testuser"));
18389

184-
// Second request immediately - should hit cache (fast)
185-
let start_time = Instant::now();
90+
// Second request immediately - should hit cache
18691
let response2 = agent.make_request(&query).await;
187-
let cache_hit_duration = start_time.elapsed();
18892
let json2: serde_json::Value = serde_json::from_str(&response2).unwrap();
189-
190-
// Verify cache hit is faster
191-
assert!(cache_hit_duration < first_duration / 2);
19293
assert_eq!(json1["VersionId"], json2["VersionId"]);
19394

19495
// Wait for TTL to expire (3 seconds + buffer)
19596
tokio::time::sleep(Duration::from_secs(4)).await;
19697

197-
// Third request after TTL expiry - should fetch from AWS again (slower)
198-
let start_time = Instant::now();
98+
// Third request after TTL expiry - should fetch from AWS again
19999
let response3 = agent.make_request(&query).await;
200-
let post_ttl_duration = start_time.elapsed();
201100
let json3: serde_json::Value = serde_json::from_str(&response3).unwrap();
202101

203-
// Post-TTL request should be slower than cache hit (goes to AWS)
204-
assert!(
205-
post_ttl_duration > cache_hit_duration * 2,
206-
"Post-TTL request should be slower. Cache hit: {:?}, Post-TTL: {:?}",
207-
cache_hit_duration,
208-
post_ttl_duration
209-
);
210-
211-
// Should still get valid response
102+
// Should still get valid response after TTL expiry
212103
assert!(json3["SecretString"].as_str().unwrap().contains("testuser"));
213104
}
214-
215-
#[tokio::test]
216-
async fn test_ttl_zero_disables_caching() {
217-
let secrets = TestSecrets::setup().await;
218-
let secret_name = secrets.secret_name(SecretType::Basic);
219-
220-
// Start agent with TTL=0 to disable caching
221-
let agent = AgentProcess::start_with_config(2775, 0).await;
222-
223-
let query = AgentQueryBuilder::default()
224-
.secret_id(&secret_name)
225-
.build()
226-
.unwrap();
227-
228-
// Make multiple requests and verify they all take significant time (go to AWS)
229-
let mut durations = Vec::new();
230-
231-
for i in 0..4 {
232-
let start_time = Instant::now();
233-
let response = agent.make_request(&query).await;
234-
let duration = start_time.elapsed();
235-
durations.push(duration);
236-
237-
let json: serde_json::Value = serde_json::from_str(&response).unwrap();
238-
assert!(json["SecretString"].as_str().unwrap().contains("testuser"));
239-
240-
// Each request should take significant time (network call to AWS)
241-
assert!(
242-
duration > Duration::from_millis(20),
243-
"Request {} should go to AWS with TTL=0, duration: {:?}",
244-
i + 1,
245-
duration
246-
);
247-
248-
// Small delay between requests to avoid overwhelming AWS
249-
if i < 3 {
250-
tokio::time::sleep(Duration::from_millis(100)).await;
251-
}
252-
}
253-
254-
// Verify no request was significantly faster (indicating cache hit)
255-
let min_duration = durations.iter().min().unwrap();
256-
257-
// All requests should be in reasonable range (no cache speedup)
258-
// Allow for network variance but ensure no sub-15ms cache hits
259-
assert!(
260-
min_duration > &Duration::from_millis(15),
261-
"Minimum duration too fast, suggests caching: {:?}. All durations: {:?}",
262-
min_duration,
263-
durations
264-
);
265-
}

0 commit comments

Comments
 (0)