feat: support for drawing
This commit is contained in:
parent
5be082a6a0
commit
7663237f57
6 changed files with 378 additions and 3 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
|
@ -101,6 +101,12 @@ version = "0.21.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
|
@ -455,6 +461,7 @@ name = "guestbook"
|
|||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"http-body-util",
|
||||
|
|
@ -466,6 +473,7 @@ dependencies = [
|
|||
"tower",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
@ -1178,7 +1186,7 @@ version = "0.11.27"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
|
|
@ -1243,7 +1251,7 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1892,6 +1900,12 @@ dependencies = [
|
|||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ toml = "0.8"
|
|||
dotenvy = "0.15"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = "0.4"
|
||||
base64 = "0.22"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
|
|
@ -22,3 +23,4 @@ tracing-subscriber = "0.3"
|
|||
tower = { version = "0.5", features = ["util"] }
|
||||
http-body-util = "0.1"
|
||||
tempfile = "3"
|
||||
urlencoding = "2"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ pub struct Config {
|
|||
pub captcha_answer: String,
|
||||
pub captcha_exact: bool,
|
||||
pub captcha_casesensitive: bool,
|
||||
pub enable_drawings: bool,
|
||||
pub label_drawing: String,
|
||||
pub canvas_width: u32,
|
||||
pub canvas_height: u32,
|
||||
pub template: Option<String>,
|
||||
pub separator: String,
|
||||
pub style: String,
|
||||
|
|
@ -38,6 +42,12 @@ impl Config {
|
|||
format!("127.0.0.1:{}", self.port)
|
||||
}
|
||||
|
||||
/// Maximum drawing file size: width * height * 4 (raw RGBA).
|
||||
/// Any valid PNG from the configured canvas will be smaller than this.
|
||||
pub fn max_drawing_bytes(&self) -> usize {
|
||||
self.canvas_width as usize * self.canvas_height as usize * 4
|
||||
}
|
||||
|
||||
pub fn from_env() -> Result<Self, String> {
|
||||
Ok(Config {
|
||||
port: env::var("BOOK_PORT")
|
||||
|
|
@ -91,6 +101,19 @@ impl Config {
|
|||
captcha_casesensitive: env::var("BOOK_CAPTCHA_CASESENSITIVE")
|
||||
.map(|v| v != "false")
|
||||
.unwrap_or(false),
|
||||
enable_drawings: env::var("BOOK_ENABLE_DRAWINGS")
|
||||
.map(|v| v != "false")
|
||||
.unwrap_or(false),
|
||||
label_drawing: env::var("BOOK_LABEL_DRAWING")
|
||||
.unwrap_or_else(|_| "Draw (optional):".into()),
|
||||
canvas_width: env::var("BOOK_CANVAS_WIDTH")
|
||||
.unwrap_or_else(|_| "400".into())
|
||||
.parse()
|
||||
.map_err(|_| "BOOK_CANVAS_WIDTH must be a number")?,
|
||||
canvas_height: env::var("BOOK_CANVAS_HEIGHT")
|
||||
.unwrap_or_else(|_| "200".into())
|
||||
.parse()
|
||||
.map_err(|_| "BOOK_CANVAS_HEIGHT must be a number")?,
|
||||
separator: env::var("BOOK_SEPARATOR")
|
||||
.unwrap_or_else(|_| "------------------------------------------------------------".into()),
|
||||
template: env::var("BOOK_TEMPLATE").ok().map(|path| {
|
||||
|
|
@ -242,4 +265,19 @@ mod tests {
|
|||
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
|
||||
env::remove_var("BOOK_ENABLE_HTML_INJECTION");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enable_drawings_default() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
env::remove_var("BOOK_ENABLE_DRAWINGS");
|
||||
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
|
||||
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
|
||||
|
||||
let config = Config::from_env().unwrap();
|
||||
assert!(!config.enable_drawings);
|
||||
assert_eq!(config.canvas_width, 400);
|
||||
assert_eq!(config.canvas_height, 200);
|
||||
assert_eq!(config.max_drawing_bytes(), 400 * 200 * 4);
|
||||
assert_eq!(config.label_drawing, "Draw (optional):");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ pub struct EntryMeta {
|
|||
pub date: String,
|
||||
#[serde(default)]
|
||||
pub website: String,
|
||||
#[serde(default)]
|
||||
pub drawing: String,
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
|
|
@ -183,4 +185,44 @@ Hello world!"#;
|
|||
let result = Entry::parse("x", "no frontmatter here");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_entry_with_drawing() {
|
||||
let contents = r#"+++
|
||||
name = "alice"
|
||||
date = "2026-04-09"
|
||||
status = "approved"
|
||||
drawing = "abc123.png"
|
||||
+++
|
||||
Hello!"#;
|
||||
let entry = Entry::parse("test", contents).unwrap();
|
||||
assert_eq!(entry.meta.drawing, "abc123.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_entry_without_drawing() {
|
||||
let contents = r#"+++
|
||||
name = "bob"
|
||||
date = "2026-04-09"
|
||||
status = "pending"
|
||||
+++
|
||||
Hi!"#;
|
||||
let entry = Entry::parse("test", contents).unwrap();
|
||||
assert_eq!(entry.meta.drawing, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_with_drawing() {
|
||||
let contents = r#"+++
|
||||
name = "alice"
|
||||
date = "2026-04-09"
|
||||
status = "approved"
|
||||
drawing = "abc123.png"
|
||||
+++
|
||||
Hello!"#;
|
||||
let entry = Entry::parse("test", contents).unwrap();
|
||||
let serialized = entry.to_file_contents();
|
||||
let reparsed = Entry::parse("test", &serialized).unwrap();
|
||||
assert_eq!(reparsed.meta.drawing, "abc123.png");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@ mod tests {
|
|||
captcha_answer: String::new(),
|
||||
captcha_exact: false,
|
||||
captcha_casesensitive: false,
|
||||
enable_drawings: false,
|
||||
label_drawing: "Draw (optional):".into(),
|
||||
canvas_width: 400,
|
||||
canvas_height: 200,
|
||||
template: None,
|
||||
separator: "---".into(),
|
||||
style: String::new(),
|
||||
|
|
@ -152,6 +156,7 @@ mod tests {
|
|||
name: name.into(),
|
||||
date: date.into(),
|
||||
website: String::new(),
|
||||
drawing: String::new(),
|
||||
status: Status::Approved,
|
||||
},
|
||||
body: body.into(),
|
||||
|
|
|
|||
276
src/web.rs
276
src/web.rs
|
|
@ -1,9 +1,13 @@
|
|||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
extract::Path as AxumPath,
|
||||
extract::State,
|
||||
response::Html,
|
||||
http::{header, StatusCode},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use base64::Engine;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -27,12 +31,16 @@ pub struct SubmitForm {
|
|||
url: String, // honeypot
|
||||
#[serde(default)]
|
||||
captcha: String,
|
||||
#[serde(default)]
|
||||
drawing: String,
|
||||
}
|
||||
|
||||
pub fn router(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/submit", post(submit))
|
||||
.route("/drawings/{filename}", get(serve_drawing))
|
||||
.layer(DefaultBodyLimit::max(2 * 1024 * 1024))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +62,34 @@ async fn index(State(state): State<Arc<AppState>>) -> Html<String> {
|
|||
Html(html)
|
||||
}
|
||||
|
||||
async fn serve_drawing(
|
||||
State(state): State<Arc<AppState>>,
|
||||
AxumPath(filename): AxumPath<String>,
|
||||
) -> Response {
|
||||
// Validate filename: only safe chars + .png
|
||||
if !filename.ends_with(".png")
|
||||
|| !filename[..filename.len() - 4]
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
|
||||
let path = state.config.data_dir.join("drawings").join(&filename);
|
||||
match std::fs::read(&path) {
|
||||
Ok(bytes) => (
|
||||
StatusCode::OK,
|
||||
[
|
||||
(header::CONTENT_TYPE, "image/png"),
|
||||
(header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
|
||||
],
|
||||
bytes,
|
||||
)
|
||||
.into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn submit(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Form(form): Form<SubmitForm>,
|
||||
|
|
@ -116,6 +152,49 @@ async fn submit(
|
|||
return Html(format!("Message is too long (max {max_msg} chars)."));
|
||||
}
|
||||
|
||||
// Process drawing if enabled and provided
|
||||
let (drawing_filename, drawing_bytes) = if state.config.enable_drawings && !form.drawing.is_empty() {
|
||||
let b64 = form.drawing
|
||||
.strip_prefix("data:image/png;base64,")
|
||||
.unwrap_or("");
|
||||
if b64.is_empty() {
|
||||
(String::new(), None)
|
||||
} else {
|
||||
let bytes = match base64::engine::general_purpose::STANDARD.decode(b64) {
|
||||
Ok(b) => b,
|
||||
Err(_) => return Html("Invalid drawing data.".to_string()),
|
||||
};
|
||||
let max = state.config.max_drawing_bytes();
|
||||
if max > 0 && bytes.len() > max {
|
||||
return Html(format!("Drawing is too large (max {} bytes).", max));
|
||||
}
|
||||
|
||||
// Validate PNG: magic bytes + IHDR dimensions match configured canvas
|
||||
if bytes.len() < 24 || &bytes[..8] != b"\x89PNG\r\n\x1a\n" {
|
||||
return Html("Invalid drawing format.".to_string());
|
||||
}
|
||||
let width = u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
|
||||
let height = u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
|
||||
if width != state.config.canvas_width || height != state.config.canvas_height {
|
||||
return Html("Invalid drawing dimensions.".to_string());
|
||||
}
|
||||
|
||||
let drawing_id = &Uuid::new_v4().to_string()[..8];
|
||||
let date_now = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
||||
let drawing_name = format!("{date_now}-{drawing_id}.png");
|
||||
let drawings_dir = state.config.data_dir.join("drawings");
|
||||
std::fs::create_dir_all(&drawings_dir).ok();
|
||||
if let Err(e) = std::fs::write(drawings_dir.join(&drawing_name), &bytes) {
|
||||
tracing::error!("failed to write drawing: {e}");
|
||||
return Html("Something went wrong. Please try again.".to_string());
|
||||
}
|
||||
(drawing_name, Some(bytes))
|
||||
}
|
||||
} else {
|
||||
(String::new(), None)
|
||||
};
|
||||
let _ = drawing_bytes;
|
||||
|
||||
let short_id = &Uuid::new_v4().to_string()[..8];
|
||||
let date = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||
let date_short = &date[..10];
|
||||
|
|
@ -127,6 +206,7 @@ async fn submit(
|
|||
name,
|
||||
date,
|
||||
website,
|
||||
drawing: drawing_filename,
|
||||
status: Status::Pending,
|
||||
},
|
||||
body: message,
|
||||
|
|
@ -152,6 +232,7 @@ mod tests {
|
|||
use super::*;
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use base64::Engine;
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
|
@ -175,6 +256,10 @@ mod tests {
|
|||
captcha_answer: String::new(),
|
||||
captcha_exact: false,
|
||||
captcha_casesensitive: false,
|
||||
enable_drawings: false,
|
||||
label_drawing: "Draw (optional):".into(),
|
||||
canvas_width: 400,
|
||||
canvas_height: 200,
|
||||
template: None,
|
||||
separator: "---".into(),
|
||||
style: String::new(),
|
||||
|
|
@ -463,4 +548,193 @@ mod tests {
|
|||
assert!(html.contains("What is 2+2?"));
|
||||
assert!(html.contains("name=\"captcha\""));
|
||||
}
|
||||
|
||||
async fn get_path(app: &Router, path: &str) -> (StatusCode, Vec<u8>) {
|
||||
let req = Request::builder()
|
||||
.uri(path)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
let status = resp.status();
|
||||
let bytes = resp.into_body().collect().await.unwrap().to_bytes().to_vec();
|
||||
(status, bytes)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serve_drawing() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let drawings_dir = dir.path().join("drawings");
|
||||
std::fs::create_dir_all(&drawings_dir).unwrap();
|
||||
let png_bytes = b"\x89PNG\r\n\x1a\nfake";
|
||||
std::fs::write(drawings_dir.join("test123.png"), png_bytes).unwrap();
|
||||
|
||||
let (status, body) = get_path(&app, "/drawings/test123.png").await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body, png_bytes);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serve_drawing_not_found() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let (status, _) = get_path(&app, "/drawings/nonexistent.png").await;
|
||||
assert_eq!(status, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serve_drawing_rejects_path_traversal() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let config = test_config(dir.path());
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let (status, _) = get_path(&app, "/drawings/../entries/secret.txt").await;
|
||||
assert_eq!(status, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
/// Build a fake but valid PNG with the given dimensions.
|
||||
fn fake_png(width: u32, height: u32) -> Vec<u8> {
|
||||
let mut png = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
|
||||
png.extend_from_slice(&13u32.to_be_bytes());
|
||||
png.extend_from_slice(b"IHDR");
|
||||
png.extend_from_slice(&width.to_be_bytes());
|
||||
png.extend_from_slice(&height.to_be_bytes());
|
||||
png.extend_from_slice(&[8, 6, 0, 0, 0]);
|
||||
png.extend_from_slice(&[0, 0, 0, 0]);
|
||||
png
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_with_drawing() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_drawings = true;
|
||||
config.canvas_width = 400;
|
||||
config.canvas_height = 200;
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let png = fake_png(400, 200);
|
||||
let drawing_data = base64::engine::general_purpose::STANDARD.encode(&png);
|
||||
let data_url = format!("data:image/png;base64,{drawing_data}");
|
||||
let body = format!(
|
||||
"name=alice&message=hello&drawing={}",
|
||||
urlencoding::encode(&data_url)
|
||||
);
|
||||
let (_, resp) = post_form(&app, &body).await;
|
||||
assert!(resp.contains("pending approval"));
|
||||
|
||||
let entries: Vec<_> = std::fs::read_dir(dir.path().join("entries"))
|
||||
.unwrap()
|
||||
.collect();
|
||||
assert_eq!(entries.len(), 1);
|
||||
let content = std::fs::read_to_string(entries[0].as_ref().unwrap().path()).unwrap();
|
||||
assert!(content.contains("drawing = "));
|
||||
|
||||
let drawings: Vec<_> = std::fs::read_dir(dir.path().join("drawings"))
|
||||
.unwrap()
|
||||
.collect();
|
||||
assert_eq!(drawings.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_without_drawing() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_drawings = true;
|
||||
let (app, _rx) = test_app(config);
|
||||
let (_, resp) = post_form(&app, "name=alice&message=hello").await;
|
||||
assert!(resp.contains("pending approval"));
|
||||
|
||||
let drawings_dir = dir.path().join("drawings");
|
||||
if drawings_dir.exists() {
|
||||
let count = std::fs::read_dir(&drawings_dir).unwrap().count();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_drawing_too_large() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_drawings = true;
|
||||
config.canvas_width = 1;
|
||||
config.canvas_height = 1;
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
// PNG with dimensions 1x1 — max_drawing_bytes() is 4, but the fake_png itself is 33 bytes
|
||||
let png = fake_png(1, 1);
|
||||
let drawing_data = base64::engine::general_purpose::STANDARD.encode(&png);
|
||||
let data_url = format!("data:image/png;base64,{drawing_data}");
|
||||
let body = format!(
|
||||
"name=alice&message=hello&drawing={}",
|
||||
urlencoding::encode(&data_url)
|
||||
);
|
||||
let (_, resp) = post_form(&app, &body).await;
|
||||
assert!(resp.contains("too large"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_drawing_rejects_non_png() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_drawings = true;
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let drawing_data = base64::engine::general_purpose::STANDARD.encode(b"not a png file at all");
|
||||
let data_url = format!("data:image/png;base64,{drawing_data}");
|
||||
let body = format!(
|
||||
"name=alice&message=hello&drawing={}",
|
||||
urlencoding::encode(&data_url)
|
||||
);
|
||||
let (_, resp) = post_form(&app, &body).await;
|
||||
assert!(resp.contains("Invalid drawing"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_drawing_rejects_wrong_dimensions() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_drawings = true;
|
||||
config.canvas_width = 400;
|
||||
config.canvas_height = 200;
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let png = fake_png(1920, 1080);
|
||||
let drawing_data = base64::engine::general_purpose::STANDARD.encode(&png);
|
||||
let data_url = format!("data:image/png;base64,{drawing_data}");
|
||||
let body = format!(
|
||||
"name=alice&message=hello&drawing={}",
|
||||
urlencoding::encode(&data_url)
|
||||
);
|
||||
let (_, resp) = post_form(&app, &body).await;
|
||||
assert!(resp.contains("Invalid drawing dimensions"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_submit_drawing_ignored_when_disabled() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_config(dir.path());
|
||||
config.enable_drawings = false;
|
||||
let (app, _rx) = test_app(config);
|
||||
|
||||
let png = fake_png(400, 200);
|
||||
let drawing_data = base64::engine::general_purpose::STANDARD.encode(&png);
|
||||
let data_url = format!("data:image/png;base64,{drawing_data}");
|
||||
let body = format!(
|
||||
"name=alice&message=hello&drawing={}",
|
||||
urlencoding::encode(&data_url)
|
||||
);
|
||||
let (_, resp) = post_form(&app, &body).await;
|
||||
assert!(resp.contains("pending approval"));
|
||||
|
||||
let entries: Vec<_> = std::fs::read_dir(dir.path().join("entries"))
|
||||
.unwrap()
|
||||
.collect();
|
||||
let content = std::fs::read_to_string(entries[0].as_ref().unwrap().path()).unwrap();
|
||||
assert!(!content.contains("drawing = \"2026"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue