1 parent
6e74b33f03
commit
3b9110c8c8
44 files changed
+3057
-128
No files matched your search
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user