v0.2.22
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-04 14:59:03 +01:00
1 parent 6e74b33f03
commit 3b9110c8c8
44 files changed
+3057 -128

No files matched your search

+257
View File
@@ -86,6 +86,12 @@ fn test_config_with_smtp(_static_dir: &str, data_dir: &str) -> Arc<AppConfig> {
sieve_timeout_secs: 10,
trusted_proxies: None,
outgoing_headers: Vec::new(),
// Existing send tests assert on the immediate, synchronous send
// outcome (status/body), not the "Undo Send" staged-dispatch flow;
// keep that behavior disabled here so they keep testing what they
// were written to test. undo_send.rs/routes/send.rs have their own
// dedicated tests for the staged-dispatch path.
undo_send_window_secs: 0,
..AppConfig::default()
})
}
@@ -2720,3 +2726,254 @@ async fn export_contact_strips_control_chars_from_filename() {
assert!(!cd.contains('\n'));
}
}
fn multipart_upload_request(
uri: &str,
filename: &str,
payload: &[u8],
auth: &[(&'static str, String)],
) -> Request<Body> {
let boundary = "rustbox-test-boundary";
let mut body = Vec::new();
body.extend_from_slice(
format!(
"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\nContent-Type: application/octet-stream\r\n\r\n"
)
.as_bytes(),
);
body.extend_from_slice(payload);
body.extend_from_slice(format!("\r\n--{boundary}--\r\n").as_bytes());
let mut req = Request::builder()
.method("POST")
.uri(uri)
.header(
"content-type",
format!("multipart/form-data; boundary={boundary}"),
)
.header("x-requested-with", "XMLHttpRequest");
for (name, value) in auth {
req = req.header(*name, value.as_str());
}
req.body(Body::from(body)).unwrap()
}
async fn error_code_of(response: axum::response::Response) -> String {
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
json["error"]["code"]
.as_str()
.unwrap_or_default()
.to_string()
}
#[tokio::test]
async fn upload_over_the_message_size_limit_is_rejected_while_streaming() {
let static_dir = setup_static_dir();
let data_dir = TempDir::new().unwrap();
let mut cfg = (*test_config_with_imap(
static_dir.path().to_str().unwrap(),
data_dir.path().to_str().unwrap(),
))
.clone();
cfg.max_attachment_size_mb = 10;
cfg.max_message_size_mb = 1;
let store = test_store();
let (browser_id, account_id, token) = setup_test_account(&store, "alice@example.com");
let user_hash = crate::auth::user_data::hash_email("alice@example.com");
provision_user_db(data_dir.path().to_str().unwrap(), &user_hash);
let app = create_router(
Arc::new(cfg),
store,
Arc::new(MockImapClient::new()),
test_smtp_client(),
test_search_engine(data_dir.path().to_str().unwrap()),
test_event_bus(),
test_idle_manager(),
);
let auth = auth_headers(&browser_id, &account_id, &token);
// A single 2 MiB file cannot fit a 1 MiB message even though it is below
// the per-attachment limit.
let response = app
.clone()
.oneshot(multipart_upload_request(
"/api/drafts/limit-draft/attachments",
"big.bin",
&vec![7u8; 2 * 1024 * 1024],
&auth,
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
assert_eq!(error_code_of(response).await, "PAYLOAD_TOO_LARGE");
// Two files that each fit but not together: the second one is refused.
for (index, expected) in [(1, StatusCode::OK), (2, StatusCode::PAYLOAD_TOO_LARGE)] {
let response = app
.clone()
.oneshot(multipart_upload_request(
"/api/drafts/limit-draft-2/attachments",
&format!("part-{index}.bin"),
&vec![1u8; 500_000],
&auth,
))
.await
.unwrap();
assert_eq!(response.status(), expected, "upload #{index}");
}
let conn =
crate::db::pool::open_user_db(data_dir.path().to_str().unwrap(), &user_hash).unwrap();
let stored = crate::db::drafts::get_draft_attachments(&conn, "limit-draft-2").unwrap();
assert_eq!(stored.len(), 1);
assert!(
crate::db::drafts::get_draft_attachments(&conn, "limit-draft")
.unwrap()
.is_empty()
);
}
#[tokio::test]
async fn send_rejects_oversized_draft_attachments_before_reading_any_blob() {
let static_dir = setup_static_dir();
let data_dir = TempDir::new().unwrap();
let config = test_config_with_smtp(
static_dir.path().to_str().unwrap(),
data_dir.path().to_str().unwrap(),
);
let store = test_store();
let (browser_id, account_id, token) = setup_test_account(&store, "alice@example.com");
let user_hash = crate::auth::user_data::hash_email("alice@example.com");
let conn =
crate::db::pool::open_user_db(data_dir.path().to_str().unwrap(), &user_hash).unwrap();
crate::db::drafts::upsert_draft(
&conn,
"huge-draft",
"",
"",
"",
"",
"",
None,
None,
None,
false,
"normal",
"html",
"none",
false,
false,
false,
)
.unwrap();
// The recorded size is far above max_message_size_mb and the blob does not
// exist: a 413 (not a 500 read error) proves nothing was loaded.
crate::db::drafts::add_draft_attachment(
&conn,
&crate::db::drafts::NewDraftAttachment {
id: "huge-att",
draft_id: "huge-draft",
filename: "huge.bin",
content_type: "application/octet-stream",
size: 200 * 1024 * 1024,
storage_key: "missing/blob",
checksum_sha256: None,
},
)
.unwrap();
let mock_smtp: Arc<dyn SmtpClient> = Arc::new(MockSmtpClient::new());
let app = create_router(
config,
store,
Arc::new(MockImapClient::new()),
mock_smtp,
test_search_engine(data_dir.path().to_str().unwrap()),
test_event_bus(),
test_idle_manager(),
);
let mut req = Request::builder()
.method("POST")
.uri("/api/messages/send")
.header("x-requested-with", "XMLHttpRequest")
.header("content-type", "application/json");
for (name, value) in auth_headers(&browser_id, &account_id, &token) {
req = req.header(name, value);
}
let response = app
.oneshot(
req.body(Body::from(
r#"{"to":["bob@example.com"],"subject":"Big","text_body":"x","draft_id":"huge-draft"}"#,
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
assert_eq!(error_code_of(response).await, "PAYLOAD_TOO_LARGE");
}
#[tokio::test]
async fn sends_with_attachments_release_their_build_slot() {
let static_dir = setup_static_dir();
let data_dir = TempDir::new().unwrap();
let mut cfg = (*test_config_with_smtp(
static_dir.path().to_str().unwrap(),
data_dir.path().to_str().unwrap(),
))
.clone();
// A single slot and a short wait: the second send only succeeds when the
// first one gave its permit back.
cfg.max_concurrent_message_builds = 1;
cfg.message_build_wait_secs = 1;
let store = test_store();
let (browser_id, account_id, token) = setup_test_account(&store, "alice@example.com");
let user_hash = crate::auth::user_data::hash_email("alice@example.com");
provision_user_db(data_dir.path().to_str().unwrap(), &user_hash);
let mock_imap = Arc::new(MockImapClient::new());
let app = create_router(
Arc::new(cfg),
store,
mock_imap.clone(),
Arc::new(MockSmtpClient::new()),
test_search_engine(data_dir.path().to_str().unwrap()),
test_event_bus(),
test_idle_manager(),
);
let auth = auth_headers(&browser_id, &account_id, &token);
for draft_id in ["slot-draft-1", "slot-draft-2"] {
let upload = app
.clone()
.oneshot(multipart_upload_request(
&format!("/api/drafts/{draft_id}/attachments"),
"note.txt",
b"hello attachment",
&auth,
))
.await
.unwrap();
assert_eq!(upload.status(), StatusCode::OK);
let mut req = Request::builder()
.method("POST")
.uri("/api/messages/send")
.header("x-requested-with", "XMLHttpRequest")
.header("content-type", "application/json");
for (name, value) in &auth {
req = req.header(*name, value.as_str());
}
let response = app
.clone()
.oneshot(
req.body(Body::from(format!(
r#"{{"to":["bob@example.com"],"subject":"With file","text_body":"see attached","draft_id":"{draft_id}"}}"#
)))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK, "send for {draft_id}");
}
assert_eq!(mock_imap.appended.lock().unwrap().len(), 2);
}