Skip to content

Commit 14877e4

Browse files
committed
tests: add e2e proxy test; optimize test container startup
1 parent 067bc5e commit 14877e4

8 files changed

Lines changed: 356 additions & 136 deletions

File tree

crates/facet-common/tests/common/minio.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,19 @@ impl MinioInstance {
102102
let config = build_minio_client_config(&self.endpoint).await;
103103
let client = Client::new(&config);
104104

105-
client
105+
let create_result = client
106106
.create_bucket()
107107
.bucket(bucket)
108108
.send()
109-
.await
110-
.expect("Failed to create bucket");
109+
.await;
110+
111+
// Ignore bucket-already-exists errors (MinIO may persist state between test runs)
112+
if let Err(e) = create_result {
113+
let error_msg = format!("{:?}", e);
114+
if !error_msg.contains("BucketAlreadyOwnedByYou") && !error_msg.contains("BucketAlreadyExists") {
115+
panic!("Failed to create bucket: {:?}", e);
116+
}
117+
}
111118

112119
client
113120
.put_object()

crates/facet-common/tests/common/postgres.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,30 @@ use testcontainers_modules::postgres::Postgres;
1616

1717
/// Helper to create a PostgreSQL container and connection pool
1818
pub async fn setup_postgres_container() -> (PgPool, testcontainers::ContainerAsync<Postgres>) {
19-
let container = Postgres::default().start().await.unwrap();
19+
let container = Postgres::default()
20+
.start()
21+
.await
22+
.unwrap();
2023

2124
let connection_string = format!(
2225
"postgresql://postgres:postgres@127.0.0.1:{}/postgres",
2326
container.get_host_port_ipv4(5432).await.unwrap()
2427
);
2528

26-
// Wait for PostgreSQL to be ready
27-
let mut retries = 0;
28-
let pool = loop {
29-
match PgPool::connect(&connection_string).await {
30-
Ok(pool) => break pool,
31-
Err(_) if retries < 30 => {
32-
retries += 1;
33-
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
29+
// Wait for PostgreSQL to be ready with timeout
30+
let pool = tokio::time::timeout(
31+
tokio::time::Duration::from_secs(5),
32+
async {
33+
loop {
34+
match PgPool::connect(&connection_string).await {
35+
Ok(pool) => break pool,
36+
Err(_) => tokio::task::yield_now().await,
37+
}
3438
}
35-
Err(e) => panic!("Failed to connect to PostgreSQL: {}", e),
3639
}
37-
};
40+
)
41+
.await
42+
.unwrap_or_else(|_| panic!("PostgreSQL failed to become ready within 5 seconds"));
3843

3944
(pool, container)
4045
}

crates/facet-common/tests/common/proxy_s3.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ impl ProxyConfig {
182182
}
183183

184184
/// Launch S3 proxy with the given configuration
185-
pub fn launch_s3proxy(config: ProxyConfig) {
185+
pub async fn launch_s3proxy(config: ProxyConfig) {
186+
let port = config.port;
187+
186188
std::thread::spawn(move || {
187189
let operation_parser = config
188190
.operation_parser
@@ -212,14 +214,28 @@ pub fn launch_s3proxy(config: ProxyConfig) {
212214
server.bootstrap();
213215

214216
let mut proxy_service = http_proxy_service(&server.configuration, proxy);
215-
proxy_service.add_tcp(&format!("0.0.0.0:{}", config.port));
217+
proxy_service.add_tcp(&format!("0.0.0.0:{}", port));
216218

217219
server.add_service(proxy_service);
218220
server.run_forever();
219221
});
220222

221-
// Give the server time to start
222-
std::thread::sleep(std::time::Duration::from_millis(500));
223+
// Wait for proxy to be ready with 5-second timeout
224+
let wait_result = tokio::time::timeout(
225+
tokio::time::Duration::from_secs(5),
226+
async {
227+
loop {
228+
if tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await.is_ok() {
229+
break;
230+
}
231+
tokio::task::yield_now().await;
232+
}
233+
}
234+
).await;
235+
236+
if wait_result.is_err() {
237+
panic!("Proxy failed to start within 5 seconds on port {}", port);
238+
}
223239
}
224240

225241
/// Add an authorization rule to an evaluator.

crates/facet-common/tests/proxy_s3.rs

Lines changed: 148 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -12,115 +12,175 @@
1212

1313
mod common;
1414

15-
use aws_config::BehaviorVersion;
16-
use aws_sdk_s3::config::{Credentials, Region};
17-
use aws_sdk_s3::Client;
18-
use facet_common::proxy::s3::UpstreamStyle;
19-
use crate::common::{
20-
get_available_port, launch_s3proxy, MinioInstance, ProxyConfig,
21-
MINIO_ACCESS_KEY, MINIO_SECRET_KEY, TEST_BUCKET, TEST_KEY,
15+
use common::{
16+
create_test_client, get_available_port, launch_s3proxy, setup_postgres_container,
17+
MinioInstance, PassthroughCredentialsResolver, ProxyConfig, TestJwtVerifier,
18+
MINIO_ACCESS_KEY, MINIO_SECRET_KEY, TEST_BUCKET,
2219
};
23-
24-
const TEST_CONTENT: &str = "Hello from Pingora proxy test!";
25-
const VALID_SESSION_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
26-
const INVALID_SESSION_TOKEN: &str = "invalid-token";
20+
use facet_common::auth::{AuthorizationEvaluator, Operation, PostgresAuthorizationEvaluator, Rule, RuleStore};
21+
use facet_common::context::ParticipantContext;
22+
use facet_common::proxy::s3::{
23+
DefaultS3OperationParser, S3Credentials, StaticParticipantContextResolver, UpstreamStyle,
24+
};
25+
use std::sync::Arc;
2726

2827
#[tokio::test]
29-
async fn test_s3_proxy_with_token_validation() {
30-
// Start MinIO container
31-
let minio = MinioInstance::launch().await;
32-
minio.setup_bucket_with_file(TEST_BUCKET, TEST_KEY, TEST_CONTENT.as_bytes()).await;
28+
async fn test_s3_proxy_end_to_end_with_postgres() {
29+
// Launch Postgres container and initialize auth evaluator
30+
let (pool, _pg_container) = setup_postgres_container().await;
31+
let auth_evaluator = Arc::new(PostgresAuthorizationEvaluator::new(pool));
32+
auth_evaluator.initialize().await.unwrap();
3333

34-
// Get an available port for the proxy
34+
// Launch MinIO container as upstream S3 server
35+
let minio = MinioInstance::launch().await;
36+
minio.setup_bucket_with_file(TEST_BUCKET, "test-file.txt", b"Hello from MinIO!").await;
37+
minio.setup_bucket_with_file(TEST_BUCKET, "data/document.pdf", b"PDF content here").await;
38+
39+
let participant_id = "user123";
40+
let scope = "agreement-456";
41+
let participant_context = ParticipantContext::builder()
42+
.identifier(participant_id)
43+
.audience("s3-proxy")
44+
.build();
45+
46+
// Create authorization rules in Postgres
47+
// Rule 1: Allow GetObject on all objects in test-bucket (note: URL path includes leading slash)
48+
let get_rule = Rule::new(
49+
scope.to_string(),
50+
vec!["s3:GetObject".to_string()],
51+
format!("^/{}/.*", TEST_BUCKET),
52+
)
53+
.unwrap();
54+
55+
// Rule 2: Allow PutObject in test-bucket/uploads/ path (note: URL path includes leading slash)
56+
let put_rule = Rule::new(
57+
scope.to_string(),
58+
vec!["s3:PutObject".to_string()],
59+
format!("^/{}/uploads/.*", TEST_BUCKET),
60+
)
61+
.unwrap();
62+
63+
auth_evaluator.save_rule(&participant_context, get_rule).await.unwrap();
64+
auth_evaluator.save_rule(&participant_context, put_rule).await.unwrap();
65+
66+
// Verify rules are stored
67+
let rules = auth_evaluator.get_rules(&participant_context).await.unwrap();
68+
assert_eq!(rules.len(), 2);
69+
70+
// Configure and launch S3 proxy with Postgres auth
3571
let proxy_port = get_available_port();
36-
launch_s3proxy(ProxyConfig::for_token_testing(
37-
proxy_port,
38-
minio.host.clone(),
39-
UpstreamStyle::PathStyle,
40-
None,
41-
VALID_SESSION_TOKEN.to_string(),
42-
"test-scope".to_string(),
43-
).await);
44-
45-
// Configure SDK to use the proxy as a reverse proxy endpoint
72+
let proxy_config = ProxyConfig {
73+
port: proxy_port,
74+
upstream_endpoint: minio.host.clone(),
75+
upstream_style: UpstreamStyle::PathStyle,
76+
proxy_domain: None,
77+
credential_resolver: Arc::new(PassthroughCredentialsResolver {
78+
credentials: S3Credentials {
79+
access_key_id: MINIO_ACCESS_KEY.to_string(),
80+
secret_key: MINIO_SECRET_KEY.to_string(),
81+
region: "us-east-1".to_string(),
82+
},
83+
}),
84+
participant_context_resolver: Arc::new(StaticParticipantContextResolver {
85+
participant_context: participant_context.clone(),
86+
}),
87+
token_verifier: Arc::new(TestJwtVerifier {
88+
scope: scope.to_string(),
89+
}),
90+
auth_evaluator: auth_evaluator.clone(),
91+
operation_parser: Some(Arc::new(DefaultS3OperationParser::new())),
92+
};
93+
94+
launch_s3proxy(proxy_config).await;
95+
96+
// Test 1: GET request - should succeed (authorized by get_rule)
4697
let proxy_url = format!("http://127.0.0.1:{}", proxy_port);
98+
let client = create_test_client(&proxy_url, Some("test-token".to_string())).await;
4799

48-
// Test Case 1: Valid token succeeds
49-
let valid_config = aws_config::defaults(BehaviorVersion::latest())
50-
.credentials_provider(Credentials::new(
51-
"",
52-
"",
53-
Some(VALID_SESSION_TOKEN.to_string()), // Valid token!
54-
None,
55-
"test",
56-
))
57-
.region(Region::new("us-east-1"))
58-
.endpoint_url(&proxy_url) // Point directly to the proxy
59-
.load()
60-
.await;
61-
62-
let valid_client = Client::new(&valid_config);
63-
64-
let result = valid_client
100+
let get_response = client
65101
.get_object()
66102
.bucket(TEST_BUCKET)
67-
.key(TEST_KEY)
103+
.key("test-file.txt")
68104
.send()
69-
.await
70-
.expect("Request with valid token should succeed");
71-
72-
let body = result.body.collect().await.expect("Failed to read body");
73-
let content = String::from_utf8(body.to_vec()).expect("Invalid UTF-8");
74-
75-
assert_eq!(content, TEST_CONTENT, "Content should match");
76-
77-
// Test Case 2: Invalid token fails
78-
let invalid_config = aws_config::defaults(BehaviorVersion::latest())
79-
.credentials_provider(Credentials::new(
80-
MINIO_ACCESS_KEY,
81-
MINIO_SECRET_KEY,
82-
Some(INVALID_SESSION_TOKEN.to_string()), // Invalid token!
83-
None,
84-
"test",
85-
))
86-
.region(Region::new("us-east-1"))
87-
.endpoint_url(&proxy_url) // Point directly to the proxy
88-
.load()
89105
.await;
90106

91-
let invalid_client = Client::new(&invalid_config);
107+
assert!(get_response.is_ok(), "GetObject should succeed with valid authorization. Error: {:?}", get_response.as_ref().err());
108+
let body = get_response.unwrap().body.collect().await.unwrap();
109+
assert_eq!(body.to_vec(), b"Hello from MinIO!");
92110

93-
let result = invalid_client
111+
// Test 2: GET another file - should succeed (authorized by get_rule)
112+
let get_response2 = client
94113
.get_object()
95114
.bucket(TEST_BUCKET)
96-
.key(TEST_KEY)
115+
.key("data/document.pdf")
97116
.send()
98117
.await;
99118

100-
assert!(result.is_err(), "Request with invalid token should fail");
101-
102-
// Test Case 3: Missing token fails
103-
let no_token_config = aws_config::defaults(BehaviorVersion::latest())
104-
.credentials_provider(Credentials::new(
105-
MINIO_ACCESS_KEY,
106-
MINIO_SECRET_KEY,
107-
None, // No token!
108-
None,
109-
"test",
110-
))
111-
.region(Region::new("us-east-1"))
112-
.endpoint_url(&proxy_url) // Point directly to the proxy
113-
.load()
114-
.await;
119+
assert!(get_response2.is_ok(), "GetObject should succeed for nested path");
120+
let body2 = get_response2.unwrap().body.collect().await.unwrap();
121+
assert_eq!(body2.to_vec(), b"PDF content here");
115122

116-
let no_token_client = Client::new(&no_token_config);
117-
118-
let result = no_token_client
119-
.get_object()
123+
// Test 3: PUT request to uploads/ - should succeed (authorized by put_rule)
124+
let put_response = client
125+
.put_object()
120126
.bucket(TEST_BUCKET)
121-
.key(TEST_KEY)
127+
.key("uploads/new-file.txt")
128+
.body(aws_sdk_s3::primitives::ByteStream::from_static(b"New content uploaded through proxy"))
122129
.send()
123130
.await;
124131

125-
assert!(result.is_err(), "Request without token should fail");
132+
assert!(put_response.is_ok(), "PutObject should succeed in uploads/ path");
133+
134+
assert!(
135+
minio.verify_object_content(TEST_BUCKET, "uploads/new-file.txt", b"New content uploaded through proxy").await,
136+
"Uploaded file should exist in MinIO with correct content"
137+
);
138+
139+
// Test 5: Verify authorization decisions are persisted in Postgres
140+
// Check that we can evaluate operations directly using the AuthorizationEvaluator trait
141+
let get_operation = Operation::builder()
142+
.scope(scope)
143+
.action("s3:GetObject")
144+
.resource(format!("/{}/test-file.txt", TEST_BUCKET))
145+
.build();
146+
147+
let authorized = <PostgresAuthorizationEvaluator as AuthorizationEvaluator>::evaluate(
148+
&*auth_evaluator,
149+
&participant_context,
150+
get_operation,
151+
)
152+
.await
153+
.unwrap();
154+
assert!(authorized, "GetObject operation should be authorized");
155+
156+
let put_operation = Operation::builder()
157+
.scope(scope)
158+
.action("s3:PutObject")
159+
.resource(format!("/{}/uploads/new-file.txt", TEST_BUCKET))
160+
.build();
161+
162+
let authorized_put = <PostgresAuthorizationEvaluator as AuthorizationEvaluator>::evaluate(
163+
&*auth_evaluator,
164+
&participant_context,
165+
put_operation,
166+
)
167+
.await
168+
.unwrap();
169+
assert!(authorized_put, "PutObject operation should be authorized for uploads/ path");
170+
171+
// Test 6: Verify unauthorized operation would be denied
172+
let delete_operation = Operation::builder()
173+
.scope(scope)
174+
.action("s3:DeleteObject")
175+
.resource(format!("/{}/test-file.txt", TEST_BUCKET))
176+
.build();
177+
178+
let unauthorized = <PostgresAuthorizationEvaluator as AuthorizationEvaluator>::evaluate(
179+
&*auth_evaluator,
180+
&participant_context,
181+
delete_operation,
182+
)
183+
.await
184+
.unwrap();
185+
assert!(!unauthorized, "DeleteObject should not be authorized");
126186
}

0 commit comments

Comments
 (0)