Skip to content

Commit bb5220b

Browse files
committed
Add tests to s3 module
1 parent 0b0d974 commit bb5220b

File tree

1 file changed

+103
-18
lines changed
  • src/webserver/database/sqlpage_functions

1 file changed

+103
-18
lines changed

src/webserver/database/sqlpage_functions/s3.rs

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::RequestInfo;
2+
use crate::app_config::AppConfig;
23
use anyhow::Context;
34
use aws_config::BehaviorVersion;
45
use aws_sdk_s3::presigning::PresigningConfig;
@@ -12,14 +13,38 @@ pub(super) async fn upload_to_s3<'a>(
1213
key: Cow<'a, str>,
1314
) -> anyhow::Result<String> {
1415
let config = &request.app_state.config;
16+
let client = get_s3_client(config).await;
17+
upload_to_s3_with_client(config, &client, bucket, data, key).await
18+
}
19+
20+
async fn upload_to_s3_with_client<'a>(
21+
config: &AppConfig,
22+
client: &aws_sdk_s3::Client,
23+
bucket: Option<Cow<'a, str>>,
24+
data: Cow<'a, str>,
25+
key: Cow<'a, str>,
26+
) -> anyhow::Result<String> {
1527
let bucket = bucket
1628
.as_deref()
1729
.or(config.s3_bucket.as_deref())
1830
.ok_or_else(|| anyhow::anyhow!("S3 bucket not configured"))?;
1931

20-
let client = get_s3_client(config).await;
32+
let body_bytes = prepare_upload_body(data.as_ref(), config).await?;
33+
34+
client
35+
.put_object()
36+
.bucket(bucket)
37+
.key(key.as_ref())
38+
.body(body_bytes.into())
39+
.send()
40+
.await
41+
.map_err(|e| anyhow::anyhow!("Failed to upload to S3: {e}"))?;
2142

22-
let body_bytes = if let Some(stripped) = data.strip_prefix("file://") {
43+
Ok(format!("s3://{bucket}/{key}"))
44+
}
45+
46+
async fn prepare_upload_body(data: &str, config: &AppConfig) -> anyhow::Result<Vec<u8>> {
47+
if let Some(stripped) = data.strip_prefix("file://") {
2348
let file_path = std::path::Path::new(stripped);
2449
// Security check: ensure the file is within the web root or allowed paths
2550
let web_root = &config.web_root;
@@ -33,7 +58,7 @@ pub(super) async fn upload_to_s3<'a>(
3358
log::error!("Failed to read file {}: {}", full_path.display(), e);
3459
e
3560
})
36-
.with_context(|| format!("Unable to read file {}", full_path.display()))?
61+
.with_context(|| format!("Unable to read file {}", full_path.display()))
3762
} else {
3863
// Assume base64
3964
use base64::Engine;
@@ -43,19 +68,8 @@ pub(super) async fn upload_to_s3<'a>(
4368
log::error!("Base64 decode failed: {e}");
4469
e
4570
})
46-
.context("Invalid base64 data")?
47-
};
48-
49-
client
50-
.put_object()
51-
.bucket(bucket)
52-
.key(key.as_ref())
53-
.body(body_bytes.into())
54-
.send()
55-
.await
56-
.map_err(|e| anyhow::anyhow!("Failed to upload to S3: {e}"))?;
57-
58-
Ok(format!("s3://{bucket}/{key}"))
71+
.context("Invalid base64 data")
72+
}
5973
}
6074

6175
pub(super) async fn get_from_s3<'a>(
@@ -64,13 +78,21 @@ pub(super) async fn get_from_s3<'a>(
6478
key: Cow<'a, str>,
6579
) -> anyhow::Result<String> {
6680
let config = &request.app_state.config;
81+
let client = get_s3_client(config).await;
82+
get_from_s3_with_client(config, &client, bucket, key).await
83+
}
84+
85+
async fn get_from_s3_with_client<'a>(
86+
config: &AppConfig,
87+
client: &aws_sdk_s3::Client,
88+
bucket: Option<Cow<'a, str>>,
89+
key: Cow<'a, str>,
90+
) -> anyhow::Result<String> {
6791
let bucket = bucket
6892
.as_deref()
6993
.or(config.s3_bucket.as_deref())
7094
.ok_or_else(|| anyhow::anyhow!("S3 bucket not configured"))?;
7195

72-
let client = get_s3_client(config).await;
73-
7496
let presigning_config = PresigningConfig::expires_in(Duration::from_secs(3600))?;
7597

7698
let presigned_request = client
@@ -107,3 +129,66 @@ async fn get_s3_client(config: &crate::app_config::AppConfig) -> aws_sdk_s3::Cli
107129
let sdk_config = loader.load().await;
108130
aws_sdk_s3::Client::new(&sdk_config)
109131
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use super::*;
136+
use crate::app_config::tests::test_config;
137+
138+
#[tokio::test]
139+
async fn test_prepare_upload_body_base64() {
140+
let config = test_config();
141+
let data = "SGVsbG8gV29ybGQ="; // "Hello World"
142+
let result = prepare_upload_body(data, &config).await.unwrap();
143+
assert_eq!(result, b"Hello World");
144+
}
145+
146+
#[tokio::test]
147+
async fn test_prepare_upload_body_invalid_base64() {
148+
let config = test_config();
149+
let data = "InvalidBase64!";
150+
let result = prepare_upload_body(data, &config).await;
151+
assert!(result.is_err());
152+
}
153+
154+
#[tokio::test]
155+
async fn test_prepare_upload_body_file_security() {
156+
let config = test_config();
157+
// Try to access a file outside the web root (assuming /tmp is outside)
158+
// Note: test_config uses current dir as web_root usually, or a temp dir.
159+
// We need to construct a path that attempts directory traversal or absolute path outside.
160+
let data = "file:///etc/passwd";
161+
let result = prepare_upload_body(data, &config).await;
162+
assert!(result.is_err());
163+
assert!(result
164+
.unwrap_err()
165+
.to_string()
166+
.contains("Security violation"));
167+
}
168+
169+
#[tokio::test]
170+
async fn test_get_from_s3_presigned() {
171+
let mut config = test_config();
172+
config.s3_bucket = Some("my-bucket".to_string());
173+
config.s3_region = Some("us-east-1".to_string());
174+
config.s3_access_key = Some("test".to_string());
175+
config.s3_secret_key = Some("test".to_string());
176+
177+
// Create a client that doesn't actually connect but is valid enough for presigning
178+
// Presigning is a local operation for the most part, but it needs credentials.
179+
let client = get_s3_client(&config).await;
180+
181+
let url = get_from_s3_with_client(
182+
&config,
183+
&client,
184+
None,
185+
Cow::Borrowed("my-file.txt"),
186+
)
187+
.await
188+
.unwrap();
189+
190+
assert!(url.contains("my-bucket"));
191+
assert!(url.contains("my-file.txt"));
192+
assert!(url.contains("X-Amz-Signature"));
193+
}
194+
}

0 commit comments

Comments
 (0)