1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-10-09 12:55:00 +02:00

Merge branch 'text-body-limit' into integration-all-three

This commit is contained in:
archivr-qa 2026-08-24 17:14:59 +02:00
commit 566b4eb7d7
No known key found for this signature in database

View file

@ -49,6 +49,10 @@ use rusqlite::OptionalExtension;
const LOGIN_WINDOW: Duration = Duration::from_secs(15 * 60);
const LOGIN_MAX_ATTEMPTS: usize = 5;
const MAX_TEXT_CAPTURE_BODY_BYTES: usize = 2 * 1024 * 1024;
// JSON can expand each body byte into a six-byte `\\u00XX` escape sequence,
// plus a small request envelope.
const MAX_TEXT_CAPTURE_REQUEST_BYTES: usize = MAX_TEXT_CAPTURE_BODY_BYTES * 6 + 64 * 1024;
// Short-lived token granting unauthenticated access to one specific artifact.
// Used so Cast / AirPlay devices (which carry no session cookie) can fetch media.
@ -279,7 +283,11 @@ pub fn app_with_state(state: AppState) -> Router {
.route("/api/archives/:archive_id/blobs/:sha256", get(serve_blob))
.route("/api/archives/:archive_id/runs", get(list_runs))
.route("/api/archives/:archive_id/captures", post(capture_handler))
.route("/api/archives/:archive_id/captures/text", post(capture_text_handler))
.route(
"/api/archives/:archive_id/captures/text",
post(capture_text_handler)
.layer(DefaultBodyLimit::max(MAX_TEXT_CAPTURE_REQUEST_BYTES)),
)
.route(
"/api/archives/:archive_id/uploads",
post(upload_handler)
@ -1414,6 +1422,9 @@ async fn capture_text_handler(
if body.body.trim().is_empty() {
return Err(ApiError::bad_request("body must not be empty"));
}
if body.body.len() > MAX_TEXT_CAPTURE_BODY_BYTES {
return Err(ApiError::bad_request("body must not exceed 2 MiB"));
}
// Determine MIME type (default to markdown)
let mime = body.mime.as_deref().unwrap_or("text/markdown");
@ -4365,6 +4376,89 @@ mod tests {
assert_eq!(json["status"], "pending");
}
#[tokio::test]
async fn text_capture_accepts_a_body_just_below_two_mebibytes() {
let dir = tempfile::tempdir().unwrap();
let (registry, _, auth_path) = make_test_registry(&dir);
let session_cookie = make_test_session(&auth_path);
let body = "a".repeat(2 * 1024 * 1024 - 1);
let response = app(registry, auth_path)
.oneshot(
Request::builder()
.method("POST")
.uri("/api/archives/test/captures/text")
.header("content-type", "application/json")
.header("cookie", &session_cookie)
.body(json_body(&serde_json::json!({
"title": "Maximum-size note",
"body": body,
"mime": "text/plain"
})))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::ACCEPTED);
}
#[tokio::test]
async fn text_capture_accepts_a_two_mebibyte_body_with_json_escaped_controls() {
let dir = tempfile::tempdir().unwrap();
let (registry, _, auth_path) = make_test_registry(&dir);
let session_cookie = make_test_session(&auth_path);
let body = "\0".repeat(2 * 1024 * 1024);
let response = app(registry, auth_path)
.oneshot(
Request::builder()
.method("POST")
.uri("/api/archives/test/captures/text")
.header("content-type", "application/json")
.header("cookie", &session_cookie)
.body(json_body(&serde_json::json!({
"title": "Escaped control note",
"body": body,
"mime": "text/plain"
})))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::ACCEPTED);
}
#[tokio::test]
async fn text_capture_rejects_a_body_over_two_mebibytes_with_validation_error() {
let dir = tempfile::tempdir().unwrap();
let (registry, _, auth_path) = make_test_registry(&dir);
let session_cookie = make_test_session(&auth_path);
let body = "a".repeat(2 * 1024 * 1024 + 1);
let response = app(registry, auth_path)
.oneshot(
Request::builder()
.method("POST")
.uri("/api/archives/test/captures/text")
.header("content-type", "application/json")
.header("cookie", &session_cookie)
.body(json_body(&serde_json::json!({
"title": "Oversized note",
"body": body,
"mime": "text/plain"
})))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let response_body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&response_body).unwrap();
assert_eq!(json["error"], "body must not exceed 2 MiB");
}
#[tokio::test]
async fn text_capture_post_preserves_intentional_whitespace() {
let dir = tempfile::tempdir().unwrap();