1 parent
96a32ea8fb
commit
a643e7e3ab
106 files changed
+2223
-4669
No files matched your search
+6
-67
@@ -17,7 +17,6 @@ use crate::imap::client::{
|
||||
use crate::smtp::client::SmtpError;
|
||||
use crate::smtp::client::mock::MockSmtpClient;
|
||||
|
||||
/// Helper: create a test AppConfig with the given static dir.
|
||||
fn test_config(_static_dir: &str) -> Arc<AppConfig> {
|
||||
Arc::new(AppConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
@@ -42,7 +41,6 @@ fn test_config(_static_dir: &str) -> Arc<AppConfig> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper: create a test AppConfig with IMAP host configured and a custom data dir.
|
||||
fn test_config_with_imap(_static_dir: &str, data_dir: &str) -> Arc<AppConfig> {
|
||||
Arc::new(AppConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
@@ -67,7 +65,6 @@ fn test_config_with_imap(_static_dir: &str, data_dir: &str) -> Arc<AppConfig> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper: create a test AppConfig with IMAP + SMTP hosts configured.
|
||||
fn test_config_with_smtp(_static_dir: &str, data_dir: &str) -> Arc<AppConfig> {
|
||||
Arc::new(AppConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
@@ -92,22 +89,18 @@ fn test_config_with_smtp(_static_dir: &str, data_dir: &str) -> Arc<AppConfig> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper: create a fresh SessionStore for tests.
|
||||
fn test_store() -> Arc<SessionStore> {
|
||||
Arc::new(SessionStore::new(Duration::from_secs(3600)))
|
||||
}
|
||||
|
||||
/// Helper: create a default mock IMAP client.
|
||||
fn test_imap_client() -> Arc<dyn ImapClient> {
|
||||
Arc::new(MockImapClient::new())
|
||||
}
|
||||
|
||||
/// Helper: create a default mock SMTP client.
|
||||
fn test_smtp_client() -> Arc<dyn SmtpClient> {
|
||||
Arc::new(MockSmtpClient::new())
|
||||
}
|
||||
|
||||
/// Helper: create a temporary static directory with an index.html.
|
||||
fn setup_static_dir() -> TempDir {
|
||||
let dir = TempDir::new().expect("should create temp dir");
|
||||
fs::write(
|
||||
@@ -118,19 +111,16 @@ fn setup_static_dir() -> TempDir {
|
||||
dir
|
||||
}
|
||||
|
||||
/// Helper: create a test SearchEngine backed by the given data directory.
|
||||
fn test_search_engine(data_dir: &str) -> Arc<crate::search::engine::SearchEngine> {
|
||||
Arc::new(crate::search::engine::SearchEngine::new(
|
||||
std::path::PathBuf::from(data_dir),
|
||||
))
|
||||
}
|
||||
|
||||
/// Helper: create a test EventBus.
|
||||
fn test_event_bus() -> Arc<crate::realtime::events::EventBus> {
|
||||
Arc::new(crate::realtime::events::EventBus::new())
|
||||
}
|
||||
|
||||
/// Helper: create a test IdleManager.
|
||||
fn test_idle_manager() -> Arc<crate::realtime::idle::IdleManager> {
|
||||
Arc::new(crate::realtime::idle::IdleManager::new())
|
||||
}
|
||||
@@ -159,8 +149,6 @@ fn extract_ip(extractor: &ProxyAwareIpExtractor, peer: &str, forwarded: Option<&
|
||||
extractor.extract(&request).unwrap()
|
||||
}
|
||||
|
||||
/// Helper: create a multi-account session for testing protected routes.
|
||||
/// Returns (browser_id, account_id, token) for use in request headers.
|
||||
fn setup_test_account(store: &SessionStore, email: &str) -> (String, String, String) {
|
||||
let browser_id = store.create_browser();
|
||||
let (token, account_id) = store.add_account_to_browser(
|
||||
@@ -178,7 +166,6 @@ fn setup_test_account(store: &SessionStore, email: &str) -> (String, String, Str
|
||||
(browser_id, account_id, token)
|
||||
}
|
||||
|
||||
/// Helper: build auth headers for multi-account requests.
|
||||
fn auth_headers(browser_id: &str, account_id: &str, token: &str) -> Vec<(&'static str, String)> {
|
||||
vec![
|
||||
(
|
||||
@@ -189,16 +176,10 @@ fn auth_headers(browser_id: &str, account_id: &str, token: &str) -> Vec<(&'stati
|
||||
]
|
||||
}
|
||||
|
||||
/// Helper: provision a user database so that route handlers can open it.
|
||||
/// Migrations are applied automatically by `open_user_db`.
|
||||
fn provision_user_db(data_dir: &str, user_hash: &str) {
|
||||
let _conn = crate::db::pool::open_user_db(data_dir, user_hash).unwrap();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Proxy-aware IP extraction tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn trusted_ipv4_proxy_uses_forwarded_client_ip() {
|
||||
let extractor = test_proxy_extractor(Some("10.0.0.0/8"));
|
||||
@@ -264,10 +245,6 @@ fn malformed_forwarded_header_falls_back_to_peer_ip() {
|
||||
assert_eq!(client_ip, "10.0.0.10".parse::<IpAddr>().unwrap());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Existing tests (updated to pass imap_client)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_health_works_with_static_fallback() {
|
||||
let dir = setup_static_dir();
|
||||
@@ -536,7 +513,7 @@ async fn login_unreachable_imap_returns_503() {
|
||||
let dir = setup_static_dir();
|
||||
let mut cfg = (*test_config(dir.path().to_str().unwrap())).clone();
|
||||
cfg.imap_host = Some("127.0.0.1".to_string());
|
||||
cfg.imap_port = 19999; // Nothing listening here
|
||||
cfg.imap_port = 19999;
|
||||
cfg.tls_enabled = false;
|
||||
let config = Arc::new(cfg);
|
||||
let store = test_store();
|
||||
@@ -694,7 +671,6 @@ async fn logout_with_valid_session_returns_200() {
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(json["status"], "logged_out");
|
||||
|
||||
// Session should be removed from the store.
|
||||
assert!(store.get(&token).is_none());
|
||||
}
|
||||
|
||||
@@ -730,7 +706,6 @@ async fn base_path_mounts_protected_data_routes_and_scopes_logout_cookies() {
|
||||
test_idle_manager(),
|
||||
);
|
||||
|
||||
// This route is outside /api/auth, where browsers default empty cookie paths.
|
||||
let mut request = Request::builder().uri(format!("{prefix}/api/folders"));
|
||||
for (name, value) in auth_headers(&browser_id, &account_id, &token) {
|
||||
request = request.header(name, value);
|
||||
@@ -795,15 +770,11 @@ async fn logout_clears_cookie() {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let set_cookie = response.headers().get_all("set-cookie");
|
||||
// Should have clearing cookies for both browser and account session
|
||||
|
||||
let cookie_str = format!("{:?}", set_cookie);
|
||||
assert!(cookie_str.contains("rustbox_browser=;") || cookie_str.contains("Max-Age=0"));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// New tests for folders and messages routes
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_folders_returns_200_with_folder_list() {
|
||||
let static_dir = setup_static_dir();
|
||||
@@ -815,7 +786,6 @@ async fn get_folders_returns_200_with_folder_list() {
|
||||
let store = test_store();
|
||||
let (browser_id, account_id, token) = setup_test_account(&store, "alice@example.com");
|
||||
|
||||
// Provision user database.
|
||||
provision_user_db(
|
||||
data_dir.path().to_str().unwrap(),
|
||||
&crate::auth::user_data::hash_email("alice@example.com"),
|
||||
@@ -1038,7 +1008,6 @@ async fn get_message_returns_sanitized_html() {
|
||||
&crate::auth::user_data::hash_email("alice@example.com"),
|
||||
);
|
||||
|
||||
// First, we need the message header in cache (fetch_headers first).
|
||||
let mock = MockImapClient::new()
|
||||
.with_headers(vec![ImapMessageHeader {
|
||||
uid: 42,
|
||||
@@ -1079,7 +1048,6 @@ async fn get_message_returns_sanitized_html() {
|
||||
test_idle_manager(),
|
||||
);
|
||||
|
||||
// First, populate the message cache by listing messages.
|
||||
let mut req1 = Request::builder()
|
||||
.uri("/api/folders/INBOX/messages")
|
||||
.header("x-requested-with", "XMLHttpRequest");
|
||||
@@ -1092,7 +1060,6 @@ async fn get_message_returns_sanitized_html() {
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// Now get the full message.
|
||||
let app2 = create_router(
|
||||
config,
|
||||
store,
|
||||
@@ -1121,19 +1088,15 @@ async fn get_message_returns_sanitized_html() {
|
||||
assert_eq!(json["uid"], 42);
|
||||
assert_eq!(json["subject"], "Test Subject");
|
||||
|
||||
// Script tag is preserved (frontend handles security via iframe sandbox).
|
||||
let html = json["html"].as_str().unwrap();
|
||||
assert!(html.contains("script"));
|
||||
assert!(html.contains("<b>bold</b>"));
|
||||
assert!(html.contains("<p>Hello</p>"));
|
||||
|
||||
// Plain text should be preserved.
|
||||
assert_eq!(json["text"], "Hello plain text");
|
||||
|
||||
// Flags should be an array.
|
||||
assert!(json["flags"].is_array());
|
||||
|
||||
// to_addresses should be an array.
|
||||
assert!(json["to_addresses"].is_array());
|
||||
}
|
||||
|
||||
@@ -1151,7 +1114,6 @@ async fn update_flags_returns_200() {
|
||||
let user_hash = crate::auth::user_data::hash_email("alice@example.com");
|
||||
provision_user_db(data_dir.path().to_str().unwrap(), &user_hash);
|
||||
|
||||
// Seed a message in the cache.
|
||||
let conn =
|
||||
crate::db::pool::open_user_db(data_dir.path().to_str().unwrap(), &user_hash).unwrap();
|
||||
crate::db::folders::upsert_folder(&conn, "INBOX", None, None, "", true, 0, 0, 0, 0).unwrap();
|
||||
@@ -1210,9 +1172,6 @@ async fn update_flags_returns_200() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_flags_rejects_invalid_flag_characters() {
|
||||
// Flag values must be IMAP atom characters (RFC 3501 §9). A value
|
||||
// containing a space — e.g. two flags accidentally joined into one
|
||||
// string — must be rejected with 400 before reaching the IMAP client.
|
||||
let static_dir = setup_static_dir();
|
||||
let data_dir = TempDir::new().unwrap();
|
||||
let config = test_config_with_imap(
|
||||
@@ -1251,8 +1210,7 @@ async fn update_flags_rejects_invalid_flag_characters() {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
// BAD_REQUEST confirms rejection at the validation layer — the IMAP
|
||||
// error path returns SERVICE_UNAVAILABLE (503), not 400.
|
||||
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(json["error"]["code"], "BAD_REQUEST");
|
||||
@@ -1314,7 +1272,6 @@ async fn move_message_returns_200() {
|
||||
let user_hash = crate::auth::user_data::hash_email("alice@example.com");
|
||||
provision_user_db(data_dir.path().to_str().unwrap(), &user_hash);
|
||||
|
||||
// Seed a message in the cache.
|
||||
let conn =
|
||||
crate::db::pool::open_user_db(data_dir.path().to_str().unwrap(), &user_hash).unwrap();
|
||||
crate::db::folders::upsert_folder(&conn, "INBOX", None, None, "", true, 0, 0, 0, 0).unwrap();
|
||||
@@ -1485,7 +1442,6 @@ async fn delete_message_returns_200() {
|
||||
let user_hash = crate::auth::user_data::hash_email("alice@example.com");
|
||||
provision_user_db(data_dir.path().to_str().unwrap(), &user_hash);
|
||||
|
||||
// Seed a message in the cache.
|
||||
let conn =
|
||||
crate::db::pool::open_user_db(data_dir.path().to_str().unwrap(), &user_hash).unwrap();
|
||||
crate::db::folders::upsert_folder(&conn, "INBOX", None, None, "", true, 0, 0, 0, 0).unwrap();
|
||||
@@ -1535,10 +1491,6 @@ async fn delete_message_returns_200() {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Attachment download tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn download_attachment_returns_binary_with_correct_headers() {
|
||||
let static_dir = setup_static_dir();
|
||||
@@ -1588,7 +1540,6 @@ async fn download_attachment_returns_binary_with_correct_headers() {
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// Verify content-type header.
|
||||
let ct = response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
@@ -1597,7 +1548,6 @@ async fn download_attachment_returns_binary_with_correct_headers() {
|
||||
.unwrap();
|
||||
assert_eq!(ct, "application/pdf");
|
||||
|
||||
// Verify content-disposition header.
|
||||
let cd = response
|
||||
.headers()
|
||||
.get("content-disposition")
|
||||
@@ -1607,7 +1557,6 @@ async fn download_attachment_returns_binary_with_correct_headers() {
|
||||
assert!(cd.contains("inline"));
|
||||
assert!(cd.contains("document.pdf"));
|
||||
|
||||
// Verify body bytes match the attachment data.
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(body.as_ref(), &attachment_data);
|
||||
}
|
||||
@@ -1618,9 +1567,6 @@ async fn download_attachment_strips_control_chars_from_filename() {
|
||||
("doc\r\nument.pdf", "document.pdf"),
|
||||
("\r\n\0\u{7f}", "attachment_0"),
|
||||
] {
|
||||
// MIME decoders can surface control characters in attachment filenames
|
||||
// via quoted-pair escapes. Control characters must be stripped before
|
||||
// the filename is used in the Content-Disposition header.
|
||||
let static_dir = setup_static_dir();
|
||||
let data_dir = TempDir::new().unwrap();
|
||||
let config = test_config_with_imap(
|
||||
@@ -1672,7 +1618,7 @@ async fn download_attachment_strips_control_chars_from_filename() {
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
// Control characters stripped; the rest of the filename is intact.
|
||||
|
||||
assert!(cd.contains(expected_filename), "got: {cd}");
|
||||
assert!(!cd.contains('\r'));
|
||||
assert!(!cd.contains('\n'));
|
||||
@@ -1777,10 +1723,6 @@ async fn download_attachment_returns_400_for_non_numeric_id() {
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Send message endpoint tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_returns_200_on_success() {
|
||||
let static_dir = setup_static_dir();
|
||||
@@ -2150,7 +2092,7 @@ async fn quota_without_server_limit_uses_cached_sizes() {
|
||||
)
|
||||
.unwrap();
|
||||
drop(conn);
|
||||
// The mock's IMAP size fallback returns zero; only SQLite has 4096.
|
||||
|
||||
let app = create_router(
|
||||
config,
|
||||
store,
|
||||
@@ -2178,8 +2120,6 @@ async fn export_contact_strips_control_chars_from_filename() {
|
||||
("Bob\nSmith", "BobSmith.vcf"),
|
||||
("\r\n\0\u{7f}", "contact.vcf"),
|
||||
] {
|
||||
// A contact name containing control characters must have them stripped
|
||||
// before the name is used in the Content-Disposition header.
|
||||
let static_dir = setup_static_dir();
|
||||
let data_dir = TempDir::new().unwrap();
|
||||
let config = test_config_with_imap(
|
||||
@@ -2192,7 +2132,6 @@ async fn export_contact_strips_control_chars_from_filename() {
|
||||
let user_hash = crate::auth::user_data::hash_email("alice@example.com");
|
||||
provision_user_db(data_dir.path().to_str().unwrap(), &user_hash);
|
||||
|
||||
// Insert a contact whose name contains a newline.
|
||||
let conn =
|
||||
crate::db::pool::open_user_db(data_dir.path().to_str().unwrap(), &user_hash).unwrap();
|
||||
let contact = crate::db::contacts::Contact {
|
||||
@@ -2246,7 +2185,7 @@ async fn export_contact_strips_control_chars_from_filename() {
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
// Control character stripped; the rest of the name is intact.
|
||||
|
||||
assert!(cd.contains(expected_filename), "got: {cd}");
|
||||
assert!(!cd.contains('\n'));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user