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

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-01 20:40:40 +01:00
1 parent cfc74d9db6
commit dacdf9c27d
76 files changed
+5209 -741

No files matched your search

+236 -30
View File
@@ -12,7 +12,8 @@ use tower_governor::key_extractor::KeyExtractor;
use crate::imap::client::mock::MockImapClient;
use crate::imap::client::{
EmailAddress, ImapAttachment, ImapError, ImapFolder, ImapMessageBody, ImapMessageHeader,
EmailAddress, FolderStatusExtended, ImapAttachment, ImapError, ImapFolder, ImapMessageBody,
ImapMessageHeader,
};
use crate::smtp::client::SmtpError;
use crate::smtp::client::mock::MockSmtpClient;
@@ -304,15 +305,18 @@ async fn disabled_optional_features_are_not_registered() {
}
#[tokio::test]
async fn auth_policy_exposes_optional_feature_switches() {
async fn auth_policy_exposes_only_login_requirements_before_authentication() {
let dir = setup_static_dir();
let mut config = (*test_config(dir.path().to_str().unwrap())).clone();
config.remember_me_enabled = true;
config.sieve_enabled = false;
config.diagnostics_enabled = false;
config.about_enabled = false;
let store = test_store();
let (browser_id, account_id, token) = setup_test_account(&store, "alice@example.com");
let app = create_router(
Arc::new(config),
test_store(),
store,
test_imap_client(),
test_smtp_client(),
test_search_engine("/tmp/oxi-test"),
@@ -321,6 +325,7 @@ async fn auth_policy_exposes_optional_feature_switches() {
);
let response = app
.clone()
.oneshot(
Request::builder()
.uri("/api/auth/policy")
@@ -332,13 +337,52 @@ async fn auth_policy_exposes_optional_feature_switches() {
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["rememberMeEnabled"], true);
for key in [
"rememberMeDays",
"sessionTimeoutHours",
"pgpEnabled",
"sieveEnabled",
"diagnosticsEnabled",
"aboutEnabled",
] {
assert!(json.get(key).is_none(), "public policy leaked {key}");
}
let unauth = app
.clone()
.oneshot(
Request::builder()
.uri("/api/auth/session-policy")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(unauth.status(), StatusCode::UNAUTHORIZED);
let mut request = Request::builder()
.uri("/api/auth/session-policy")
.header("x-requested-with", "XMLHttpRequest");
for (name, value) in auth_headers(&browser_id, &account_id, &token) {
request = request.header(name, value);
}
let response = app
.oneshot(request.body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["sieveEnabled"], false);
assert_eq!(json["diagnosticsEnabled"], false);
assert_eq!(json["aboutEnabled"], false);
assert!(json.get("rememberMeDays").is_none());
assert!(json.get("sessionTimeoutHours").is_none());
}
#[tokio::test]
async fn security_headers_are_opt_in() {
async fn security_headers_are_opt_in_and_content_type_aware() {
let dir = setup_static_dir();
let app = create_router(
test_config(dir.path().to_str().unwrap()),
@@ -350,43 +394,134 @@ async fn security_headers_are_opt_in() {
test_idle_manager(),
);
let response = app
.oneshot(
Request::builder()
.uri("/api/health")
.body(Body::empty())
.unwrap(),
)
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert!(response.headers().get("content-security-policy").is_none());
let mut config = (*test_config(dir.path().to_str().unwrap())).clone();
config.security_headers_enabled = true;
let app = create_router(
Arc::new(config),
assert!(response.headers().get("x-content-type-options").is_none());
let disabled_api = create_router(
test_config(dir.path().to_str().unwrap()),
test_store(),
test_imap_client(),
test_smtp_client(),
test_search_engine("/tmp/oxi-test"),
test_event_bus(),
test_idle_manager(),
);
let response = app
.oneshot(
Request::builder()
.uri("/api/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.headers()["x-frame-options"], "DENY");
)
.oneshot(
Request::builder()
.uri("/api/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert!(
response.headers()["content-security-policy"]
.to_str()
.unwrap()
.contains("default-src 'self'")
disabled_api
.headers()
.get("x-content-type-options")
.is_none()
);
assert!(
disabled_api
.headers()
.get("cross-origin-resource-policy")
.is_none()
);
assert!(
disabled_api
.headers()
.get("content-security-policy")
.is_none()
);
assert!(disabled_api.headers().get("x-frame-options").is_none());
let mut config = (*test_config(dir.path().to_str().unwrap())).clone();
config.security_headers_enabled = true;
let config = Arc::new(config);
let html = create_router(
config.clone(),
test_store(),
test_imap_client(),
test_smtp_client(),
test_search_engine("/tmp/oxi-test"),
test_event_bus(),
test_idle_manager(),
)
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(html.headers()["x-content-type-options"], "nosniff");
assert_eq!(html.headers()["x-frame-options"], "DENY");
assert_eq!(html.headers()["referrer-policy"], "no-referrer");
assert_eq!(
html.headers()["permissions-policy"],
"camera=(), microphone=(), geolocation=()"
);
assert_eq!(html.headers()["cross-origin-opener-policy"], "same-origin");
let html_csp = html.headers()["content-security-policy"].to_str().unwrap();
assert!(html_csp.contains("default-src 'self'"));
assert!(html_csp.contains("script-src 'self';"));
assert!(html_csp.contains("script-src-attr 'none'"));
assert!(html_csp.contains("style-src 'self'; style-src-attr 'unsafe-inline'"));
assert!(!html_csp.contains("style-src 'self' 'unsafe-inline'"));
assert!(!html_csp.contains("unsafe-inline'; script-src"));
assert!(!html_csp.contains("script-src 'self' 'unsafe-inline'"));
let css = create_router(
config.clone(),
test_store(),
test_imap_client(),
test_smtp_client(),
test_search_engine("/tmp/oxi-test"),
test_event_bus(),
test_idle_manager(),
)
.oneshot(
Request::builder()
.uri("/assets/app.css")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(css.headers()["x-content-type-options"], "nosniff");
assert_eq!(css.headers()["referrer-policy"], "no-referrer");
assert_eq!(css.headers()["cross-origin-resource-policy"], "same-origin");
assert!(css.headers().get("x-frame-options").is_none());
assert!(css.headers().get("permissions-policy").is_none());
assert!(css.headers().get("cross-origin-opener-policy").is_none());
assert!(css.headers().get("content-security-policy").is_none());
let api = create_router(
config,
test_store(),
test_imap_client(),
test_smtp_client(),
test_search_engine("/tmp/oxi-test"),
test_event_bus(),
test_idle_manager(),
)
.oneshot(
Request::builder()
.uri("/api/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(api.headers()["x-content-type-options"], "nosniff");
assert_eq!(api.headers()["cache-control"], "no-store");
assert_eq!(api.headers()["cross-origin-resource-policy"], "same-origin");
assert_eq!(
api.headers()["content-security-policy"],
"frame-ancestors 'none'"
);
assert_eq!(api.headers()["x-frame-options"], "DENY");
assert!(api.headers().get("referrer-policy").is_none());
assert!(api.headers().get("permissions-policy").is_none());
assert!(api.headers().get("cross-origin-opener-policy").is_none());
}
#[tokio::test]
@@ -945,6 +1080,77 @@ async fn get_folders_returns_200_with_folder_list() {
assert_eq!(folders[1]["name"], "Sent");
}
#[tokio::test]
async fn folder_refresh_uses_server_counts_without_relisting_fresh_folder_tree() {
let static_dir = setup_static_dir();
let data_dir = TempDir::new().unwrap();
let config = test_config_with_imap(
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");
provision_user_db(data_dir.path().to_str().unwrap(), &user_hash);
let conn = crate::db::pool::open_user_db(data_dir.path().to_str().unwrap(), &user_hash)
.expect("open user DB");
crate::db::folders::upsert_folder(
&conn,
"INBOX",
Some("/"),
None,
"\\HasNoChildren",
true,
3444,
1,
1,
10,
)
.unwrap();
crate::db::folders::touch_all_folders(&conn).unwrap();
drop(conn);
let mock = MockImapClient::new().with_folder_status_extended(FolderStatusExtended {
uid_validity: 1,
exists: 3444,
uid_next: 3445,
unseen: 12,
highest_modseq: 11,
});
let app = create_router(
config,
store,
Arc::new(mock),
test_smtp_client(),
test_search_engine(data_dir.path().to_str().unwrap()),
test_event_bus(),
test_idle_manager(),
);
let mut request = Request::builder()
.uri("/api/folders?refresh=true")
.header("x-requested-with", "XMLHttpRequest");
for (name, value) in auth_headers(&browser_id, &account_id, &token) {
request = request.header(name, value);
}
let response = app
.oneshot(request.body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
let inbox = json["folders"]
.as_array()
.unwrap()
.iter()
.find(|folder| folder["name"] == "INBOX")
.expect("INBOX remains in the cached folder tree");
assert_eq!(inbox["total_count"], 3444);
assert_eq!(inbox["unread_count"], 12);
}
#[tokio::test]
async fn get_folders_returns_503_when_imap_fails() {
let static_dir = setup_static_dir();