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

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 12:06:44 +01:00
1 parent 96a32ea8fb
commit a643e7e3ab
106 files changed
+2223 -4669

No files matched your search

+6 -67
View File
@@ -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'));
}