what-core 1.7.0

Core framework for What - an HTML-first web framework powered by Rust
Documentation
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("/"));

    // Follow redirect with cookie to see flash
    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>"##);
    // Default config has uploads disabled
    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;

    // Verify the file was saved in the uploads directory
    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");

    // Verify file content
    let saved_path = entries[0].path();
    let content = std::fs::read_to_string(&saved_path).unwrap();
    assert_eq!(content, "Hello, world!");

    // Verify filename has UUID and original extension
    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);

    // Upload a file
    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;

    // Find the saved file
    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();

    // Fetch via /uploads/ path
    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(); // 100 bytes max
    let (_dir, state) = proj.build_state_with_config(config);
    let router = create_router(state);

    // Upload a file larger than 100 bytes
    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;

    // Should have 2 files in uploads
    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);

    // No files should be in uploads
    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");
}