mod common;
use axum::http::StatusCode;
use common::*;
use what_core::server::create_router;
#[tokio::test]
async fn upload_creates_record_with_file_path() {
let proj = TestProject::new();
proj.add_page("index.html", r##"<h1>#flash.success|default:"none"#</h1>"##);
let (_dir, state) = proj.build_state_with_uploads();
let router = create_router(state);
let resp = post_multipart(
&router,
"/w-upload/photos?w-redirect=/",
vec![
("title", MultipartField::Text("My Photo")),
(
"file",
MultipartField::File {
filename: "test.png",
content_type: "image/png",
data: b"fake-png-data",
},
),
],
)
.await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
assert_eq!(resp.location(), Some("/"));
let cookie = resp.set_cookie().expect("should set session cookie");
let resp = get_with_headers(&router, "/", vec![("cookie", cookie)]).await;
resp.assert_contains("Item created in photos");
}
#[tokio::test]
async fn upload_disabled_returns_error_flash() {
let proj = TestProject::new();
proj.add_page("form.html", r##"<p>#flash.error|default:"no-error"#</p>"##);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = post_multipart_with_headers(
&router,
"/w-upload/photos?w-redirect=/form",
vec![(
"file",
MultipartField::File {
filename: "test.png",
content_type: "image/png",
data: b"fake-png-data",
},
)],
vec![("referer", "/form")],
)
.await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let cookie = resp.set_cookie().expect("should set session cookie");
let resp = get_with_headers(&router, "/form", vec![("cookie", cookie)]).await;
resp.assert_contains("not enabled");
}
#[tokio::test]
async fn upload_saves_file_to_uploads_dir() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>ok</h1>");
let (dir, state) = proj.build_state_with_uploads();
let router = create_router(state);
post_multipart(
&router,
"/w-upload/files?w-redirect=/",
vec![(
"doc",
MultipartField::File {
filename: "readme.txt",
content_type: "text/plain",
data: b"Hello, world!",
},
)],
)
.await;
let uploads_dir = dir.path().join("uploads");
let entries: Vec<_> = std::fs::read_dir(&uploads_dir)
.expect("uploads dir should exist")
.filter_map(|e| e.ok())
.collect();
assert_eq!(entries.len(), 1, "Should have exactly one uploaded file");
let saved_path = entries[0].path();
let content = std::fs::read_to_string(&saved_path).unwrap();
assert_eq!(content, "Hello, world!");
let fname = saved_path.file_name().unwrap().to_str().unwrap();
assert!(
fname.ends_with(".txt"),
"Should preserve .txt extension, got: {}",
fname
);
assert!(fname.len() > 10, "Should have UUID prefix");
}
#[tokio::test]
async fn upload_file_served_at_uploads_path() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>ok</h1>");
let (dir, state) = proj.build_state_with_uploads();
let router = create_router(state);
post_multipart(
&router,
"/w-upload/docs?w-redirect=/",
vec![(
"file",
MultipartField::File {
filename: "hello.txt",
content_type: "text/plain",
data: b"file-content-here",
},
)],
)
.await;
let uploads_dir = dir.path().join("uploads");
let entries: Vec<_> = std::fs::read_dir(&uploads_dir)
.unwrap()
.filter_map(|e| e.ok())
.collect();
let saved_name = entries[0].file_name();
let resp = get(
&router,
&format!("/uploads/{}", saved_name.to_str().unwrap()),
)
.await;
assert_eq!(resp.status, StatusCode::OK);
assert_eq!(resp.body, "file-content-here");
}
#[tokio::test]
async fn upload_rejects_oversized_file() {
let proj = TestProject::new();
proj.add_page("form.html", r##"<p>#flash.error|default:"no-error"#</p>"##);
let mut config = what_core::Config::default();
config.uploads.enabled = true;
config.uploads.max_size = "100".to_string(); let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let big_data = vec![0u8; 200];
let resp = post_multipart_with_headers(
&router,
"/w-upload/files?w-redirect=/form",
vec![(
"file",
MultipartField::File {
filename: "big.bin",
content_type: "application/octet-stream",
data: &big_data,
},
)],
vec![("referer", "/form")],
)
.await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let cookie = resp.set_cookie().expect("should set session cookie");
let resp = get_with_headers(&router, "/form", vec![("cookie", cookie)]).await;
resp.assert_contains("exceeds maximum size");
}
#[tokio::test]
async fn upload_rejects_disallowed_type() {
let proj = TestProject::new();
proj.add_page("form.html", r##"<p>#flash.error|default:"no-error"#</p>"##);
let mut config = what_core::Config::default();
config.uploads.enabled = true;
config.uploads.allowed_types = vec!["image/*".to_string()];
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = post_multipart_with_headers(
&router,
"/w-upload/files?w-redirect=/form",
vec![(
"file",
MultipartField::File {
filename: "script.js",
content_type: "application/javascript",
data: b"alert('hi')",
},
)],
vec![("referer", "/form")],
)
.await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let cookie = resp.set_cookie().expect("should set session cookie");
let resp = get_with_headers(&router, "/form", vec![("cookie", cookie)]).await;
resp.assert_contains("not allowed");
}
#[tokio::test]
async fn upload_allows_matching_type() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>ok</h1>");
let mut config = what_core::Config::default();
config.uploads.enabled = true;
config.uploads.allowed_types = vec!["image/*".to_string()];
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = post_multipart(
&router,
"/w-upload/photos?w-redirect=/",
vec![(
"file",
MultipartField::File {
filename: "photo.jpg",
content_type: "image/jpeg",
data: b"fake-jpeg",
},
)],
)
.await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
assert_eq!(resp.location(), Some("/"));
}
#[tokio::test]
async fn upload_with_multiple_files() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>ok</h1>");
let (dir, state) = proj.build_state_with_uploads();
let router = create_router(state);
post_multipart(
&router,
"/w-upload/gallery?w-redirect=/",
vec![
("title", MultipartField::Text("My Gallery")),
(
"photo1",
MultipartField::File {
filename: "a.png",
content_type: "image/png",
data: b"data-a",
},
),
(
"photo2",
MultipartField::File {
filename: "b.jpg",
content_type: "image/jpeg",
data: b"data-b",
},
),
],
)
.await;
let uploads_dir = dir.path().join("uploads");
let entries: Vec<_> = std::fs::read_dir(&uploads_dir)
.unwrap()
.filter_map(|e| e.ok())
.collect();
assert_eq!(entries.len(), 2, "Should have two uploaded files");
}
#[tokio::test]
async fn upload_empty_file_field_skipped() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>ok</h1>");
let (dir, state) = proj.build_state_with_uploads();
let router = create_router(state);
let resp = post_multipart(
&router,
"/w-upload/items?w-redirect=/",
vec![
("title", MultipartField::Text("No file")),
(
"file",
MultipartField::File {
filename: "",
content_type: "application/octet-stream",
data: b"",
},
),
],
)
.await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let uploads_dir = dir.path().join("uploads");
let entries: Vec<_> = std::fs::read_dir(&uploads_dir)
.unwrap()
.filter_map(|e| e.ok())
.collect();
assert_eq!(entries.len(), 0, "Empty file fields should be skipped");
}