improvements

This commit is contained in:
Mateusz Gruszczyński
2026-08-05 09:53:28 +02:00
parent 248f4a3977
commit a9911da9a3
42 changed files with 1667 additions and 728 deletions
+53
View File
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
fn headers_with_guest_id(guest_id: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(
header::COOKIE,
HeaderValue::from_str(&format!("rustpad_guest_id={guest_id}"))
.expect("valid cookie header"),
);
headers
}
#[test]
fn guest_owner_requires_the_original_browser_identifier() {
let owner_id = "0123456789abcdef0123456789abcdef";
let owner_headers = headers_with_guest_id(owner_id);
let visitor_headers = headers_with_guest_id("fedcba9876543210fedcba9876543210");
assert!(guest_owner_is_requester(&owner_headers, Some(owner_id)));
assert!(!guest_owner_is_requester(&visitor_headers, Some(owner_id)));
assert!(!guest_owner_is_requester(&HeaderMap::new(), Some(owner_id)));
}
#[test]
fn invalid_guest_identifier_does_not_grant_ownership() {
let headers = headers_with_guest_id("too-short");
assert!(!guest_owner_is_requester(&headers, Some("too-short")));
}
#[test]
fn only_an_owner_can_set_the_first_password() {
assert!(can_set_resource_password(false, true, false));
assert!(can_set_resource_password(false, false, true));
assert!(!can_set_resource_password(false, false, false));
assert!(!can_set_resource_password(true, true, true));
}
#[test]
fn settings_allow_owner_or_verified_password_holder() {
assert!(can_manage_resource_settings(true, false, false));
assert!(can_manage_resource_settings(false, true, false));
assert!(can_manage_resource_settings(false, false, true));
assert!(!can_manage_resource_settings(false, false, false));
}
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::{content_references_file, content_references_stored_file, is_safe_inline_image_mime};
#[test]
fn only_raster_images_are_inline() {
assert!(is_safe_inline_image_mime("image/png"));
assert!(is_safe_inline_image_mime("image/jpeg"));
assert!(!is_safe_inline_image_mime("image/svg+xml"));
assert!(!is_safe_inline_image_mime("text/html"));
assert!(!is_safe_inline_image_mime("application/xml"));
}
#[test]
fn extended_image_alias_is_still_attached() {
assert!(content_references_file(
"[image=photo.jpg,Photo,a=left,size=640x400]",
"photo.jpg",
"/f/token/photo.jpg",
));
assert!(content_references_file(
"[file=report.pdf,Quarterly report]",
"report.pdf",
"/f/token/report.pdf",
));
}
#[test]
fn attachment_references_survive_origin_changes() {
let stored = "/f/token/image.png";
assert!(content_references_stored_file(
"![diagram](https://old-files.example.com/f/token/image.png)",
"image.png",
stored,
None,
));
assert!(content_references_stored_file(
"![diagram](/f/token/image.png)",
"image.png",
"https://old-files.example.com/f/token/image.png",
Some("https://new-files.example.com"),
));
}
+121
View File
@@ -0,0 +1,121 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::{ResponseHeaderPolicy, apply_response_headers, response_header_policy};
use axum::http::{HeaderMap, HeaderValue, header};
#[test]
fn classifies_assets_and_icons_as_static_assets() {
for path in [
"/assets/app.js",
"/assets",
"/favicon.ico",
"/icons/favicon.svg",
"/icons/missing.svg",
] {
assert_eq!(
response_header_policy(path),
ResponseHeaderPolicy::StaticAsset
);
}
}
#[test]
fn classifies_file_routes_as_files() {
for path in ["/f", "/f/token/image.png"] {
assert_eq!(response_header_policy(path), ResponseHeaderPolicy::File);
}
}
#[test]
fn classifies_other_routes_as_application() {
for path in [
"/",
"/api/auth/me",
"/static/missing.css",
"/files/legacy/image.png",
"/unknown",
] {
assert_eq!(
response_header_policy(path),
ResponseHeaderPolicy::Application
);
}
}
#[test]
fn static_asset_policy_only_adds_nosniff() {
let mut headers = HeaderMap::new();
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=3600"),
);
apply_response_headers(ResponseHeaderPolicy::StaticAsset, &mut headers);
assert_eq!(headers.len(), 2);
assert_eq!(headers[header::X_CONTENT_TYPE_OPTIONS], "nosniff");
assert!(!headers.contains_key("x-frame-options"));
assert!(!headers.contains_key("cross-origin-opener-policy"));
assert!(!headers.contains_key("cross-origin-resource-policy"));
assert!(!headers.contains_key("referrer-policy"));
assert!(!headers.contains_key("permissions-policy"));
}
#[test]
fn file_policy_keeps_file_headers_without_document_policies() {
let mut headers = HeaderMap::new();
headers.insert(
"content-security-policy",
HeaderValue::from_static("default-src 'none'; sandbox"),
);
headers.insert(
header::CONTENT_DISPOSITION,
HeaderValue::from_static("attachment; filename=\"manual.pdf\""),
);
apply_response_headers(ResponseHeaderPolicy::File, &mut headers);
assert_eq!(headers[header::X_CONTENT_TYPE_OPTIONS], "nosniff");
assert_eq!(
headers["content-security-policy"],
"default-src 'none'; sandbox"
);
assert!(headers.contains_key(header::CONTENT_DISPOSITION));
assert!(!headers.contains_key("x-frame-options"));
assert!(!headers.contains_key("cross-origin-opener-policy"));
assert!(!headers.contains_key("cross-origin-resource-policy"));
assert!(!headers.contains_key("referrer-policy"));
assert!(!headers.contains_key("permissions-policy"));
}
#[test]
fn application_policy_preserves_handler_headers() {
let mut headers = HeaderMap::new();
headers.insert(
"content-security-policy",
HeaderValue::from_static("default-src 'none'; sandbox"),
);
apply_response_headers(ResponseHeaderPolicy::Application, &mut headers);
assert_eq!(
headers["content-security-policy"],
"default-src 'none'; sandbox"
);
assert_eq!(headers["x-frame-options"], "DENY");
assert_eq!(headers["cross-origin-opener-policy"], "same-origin");
assert_eq!(headers["cross-origin-resource-policy"], "same-origin");
assert_eq!(headers[header::X_CONTENT_TYPE_OPTIONS], "nosniff");
assert_eq!(
headers["referrer-policy"],
"strict-origin-when-cross-origin"
);
assert!(headers.contains_key("permissions-policy"));
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[test]
fn share_exchange_redirects_to_canonical_resource_path() {
let uri: Uri = "/w/private?view=all&share=secret&page=2".parse().unwrap();
assert_eq!(canonical_resource_url(&uri), "/w/private?view=all&page=2");
}
#[test]
fn encoded_or_repeated_parameters_are_parsed_without_rejection() {
let uri: Uri = "/w/private?%73hare=one&share=two".parse().unwrap();
assert_eq!(
share_token_from_query(uri.query()),
(true, Some("one".into()))
);
assert_eq!(canonical_resource_url(&uri), "/w/private");
}
#[test]
fn malformed_share_parameter_is_still_removed_from_the_url() {
let uri: Uri = "/w/private?share=%ZZ&keep=no".parse().unwrap();
assert_eq!(share_token_from_query(uri.query()), (true, None));
assert_eq!(canonical_resource_url(&uri), "/w/private?keep=no");
}
+92
View File
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
fn operation(components: Vec<OperationComponent>) -> TextOperation {
TextOperation { components }
}
#[test]
fn concurrent_insertions_have_stable_order() {
let left = operation(vec![
OperationComponent::Retain { count: 1 },
OperationComponent::Insert {
text: "X".into(),
owners: Vec::new(),
},
OperationComponent::Retain { count: 1 },
]);
let right = operation(vec![
OperationComponent::Retain { count: 1 },
OperationComponent::Insert {
text: "Y".into(),
owners: Vec::new(),
},
OperationComponent::Retain { count: 1 },
]);
let left_prime = transform_operation(&left, &right, true).unwrap();
let right_prime = transform_operation(&right, &left, false).unwrap();
let after_right = apply_operation_to_document("aYb", "[]", &left_prime, &[])
.unwrap()
.0;
let after_left = apply_operation_to_document("aXb", "[]", &right_prime, &[])
.unwrap()
.0;
assert_eq!(after_right, "aXYb");
assert_eq!(after_left, "aXYb");
}
#[test]
fn utf16_offsets_support_emoji() {
let operation = operation(vec![
OperationComponent::Retain { count: 3 },
OperationComponent::Insert {
text: "x".into(),
owners: Vec::new(),
},
OperationComponent::Retain { count: 1 },
]);
let result = apply_operation_to_document("A😀B", "[]", &operation, &[])
.unwrap()
.0;
assert_eq!(result, "A😀xB");
}
#[test]
fn operation_from_edit_preserves_utf16_boundaries() {
let operation = operation_from_edit("A😀B", "A😀xB", "[]");
let result = apply_operation_to_document("A😀B", "[]", &operation, &[])
.unwrap()
.0;
assert_eq!(result, "A😀xB");
}
#[test]
fn acknowledgements_survive_history_compaction() {
let mut document = CollaborativeDocument::new(String::new(), "[]".into(), 0);
for update_id in 1..=MAX_OPERATION_HISTORY as u64 + 8 {
let base_revision_id = document.revision_id;
let revision_id = base_revision_id + 1;
document.revision_id = revision_id;
document.record(AppliedOperation {
base_revision_id,
revision_id,
client_id: "client-123".into(),
update_id,
operation: TextOperation::default(),
owner_replacements: Vec::new(),
});
}
assert!(document.has_applied_update("client-123", 1));
assert_eq!(
document.acknowledged_updates("client-123"),
vec![MAX_OPERATION_HISTORY as u64 + 8]
);
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::{megabytes_to_bytes, multipart_body_limit_bytes};
#[test]
fn converts_upload_megabytes_to_bytes() {
assert_eq!(megabytes_to_bytes("LIMIT", 5).unwrap(), 5 * 1024 * 1024);
}
#[test]
fn rejects_overflowing_upload_limit() {
assert!(megabytes_to_bytes("LIMIT", u64::MAX).is_err());
}
#[test]
fn multipart_limit_uses_user_limit_when_guest_uploads_are_disabled() {
assert_eq!(
multipart_body_limit_bytes(20 * 1024 * 1024, false, 50 * 1024 * 1024),
21 * 1024 * 1024
);
}
#[test]
fn multipart_limit_uses_larger_enabled_guest_limit() {
assert_eq!(
multipart_body_limit_bytes(20 * 1024 * 1024, true, 50 * 1024 * 1024),
51 * 1024 * 1024
);
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[test]
fn detects_security_from_standard_ports() {
assert_eq!(smtp_security_for_port(25), SmtpSecurity::None);
assert_eq!(smtp_security_for_port(465), SmtpSecurity::Tls);
assert_eq!(smtp_security_for_port(587), SmtpSecurity::StartTls);
assert_eq!(smtp_security_for_port(2525), SmtpSecurity::None);
}
#[test]
fn accepts_supported_security_modes() {
assert_eq!(parse_smtp_security("none").unwrap(), SmtpSecurity::None);
assert_eq!(
parse_smtp_security("starttls").unwrap(),
SmtpSecurity::StartTls
);
assert_eq!(parse_smtp_security("tls").unwrap(), SmtpSecurity::Tls);
assert!(parse_smtp_security("auto").is_err());
}
#[test]
fn normalizes_smtp_from() {
assert_eq!(
normalize_smtp_from(" \"RustPad <rustpad@notes.example>\" ".to_owned()).unwrap(),
"RustPad <rustpad@notes.example>"
);
assert!(normalize_smtp_from("RustPad".to_owned()).is_err());
}
+62
View File
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
fn workspace(password_hash: Option<String>) -> Workspace {
Workspace {
id: 1,
slug: "private-workspace".into(),
title: "Private workspace".into(),
password_hash,
created_at: String::new(),
updated_at: String::new(),
is_private: 1,
created_by_guest_id: None,
}
}
fn pad(password_hash: Option<String>) -> Pad {
Pad {
id: 1,
slug: "private-pad".into(),
title: "Private pad".into(),
content: String::new(),
password_hash,
created_at: String::new(),
updated_at: String::new(),
is_private: 1,
created_by_guest_id: None,
}
}
#[test]
fn missing_password_does_not_grant_password_access() {
assert!(!verify_workspace_password(&workspace(None), None));
assert!(!verify_workspace_password(
&workspace(None),
Some("anything")
));
assert!(!verify_pad_password(&pad(None), None));
assert!(!verify_pad_password(&pad(None), Some("anything")));
}
#[test]
fn configured_password_is_verified() {
let workspace = workspace(Some(hash_password("workspace-secret")));
assert!(verify_workspace_password(
&workspace,
Some("workspace-secret")
));
assert!(!verify_workspace_password(&workspace, Some("wrong")));
let pad = pad(Some(hash_password("pad-secret")));
assert!(verify_pad_password(&pad, Some("pad-secret")));
assert!(!verify_pad_password(&pad, Some("wrong")));
}
+66
View File
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[test]
fn normalizes_bare_domain_and_http_origins() {
assert_eq!(
normalize_public_base(Some("files.note.example.com".into())).unwrap(),
Some("https://files.note.example.com".into())
);
assert_eq!(
normalize_public_base(Some("http://localhost:3001/".into())).unwrap(),
Some("http://localhost:3001".into())
);
assert_eq!(normalize_public_base(Some(" ".into())).unwrap(), None);
}
#[test]
fn rejects_non_origin_public_urls() {
assert!(normalize_public_base(Some("ftp://files.example.com".into())).is_err());
assert!(normalize_public_base(Some("https://files.example.com/path".into())).is_err());
assert!(normalize_public_base(Some("https://user@files.example.com".into())).is_err());
assert!(normalize_public_base(Some("files.example.com\\path".into())).is_err());
}
#[test]
fn extracts_canonical_path_from_relative_and_absolute_urls() {
assert_eq!(
canonical_file_path("/f/token/image.png"),
Some("/f/token/image.png".into())
);
assert_eq!(
canonical_file_path("https://files.example.com/f/token/image.png"),
Some("/f/token/image.png".into())
);
assert_eq!(
canonical_file_path("https://files.example.com/f/token/image.png?download=1"),
Some("/f/token/image.png".into())
);
assert_eq!(canonical_file_path("/files/token/image.png"), None);
}
#[test]
fn switches_between_custom_origin_and_application_path() {
let stored = "/f/token/manual.pdf";
assert_eq!(public_file_url(None, stored), stored);
assert_eq!(
public_file_url(Some("https://files.example.com"), stored),
"https://files.example.com/f/token/manual.pdf"
);
assert_eq!(
public_file_url(None, "https://old.example.com/f/token/manual.pdf"),
stored
);
assert_eq!(
public_file_url(Some("https://files.example.com"), "/invalid/path"),
"/invalid/path"
);
}
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::startup_credential;
#[test]
fn startup_credential_contains_product_identity() {
let credential = startup_credential();
assert!(credential.contains(&format!("RustPad {}", env!("CARGO_PKG_VERSION"))));
assert!(credential.contains("Mateusz Gruszczyński @linuxiarz.pl"));
assert!(
credential.contains("https://git.linuxiarz.pl/gru/rustpad/src/branch/master/LICENSE.md")
);
}
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[test]
fn every_backend_has_explicit_queries() {
for query in [
Q001,
Q003,
Q004,
Q011,
Q021,
Q033,
USER_LIST_WORKSPACES,
USER_LIST_PADS,
SHARE_LINK_SESSION_SOURCE,
SHARE_SESSION_INSERT,
SHARE_SESSION_PERMISSION,
SHARE_SESSIONS_DELETE_BY_LINK,
SHARE_SESSIONS_DELETE_EXPIRED,
PAD_PUBLIC_PAGE_DISABLED,
NOTE_PUBLIC_PAGE_DISABLED,
] {
assert!(!get(DatabaseKind::Sqlite, query).is_empty());
assert!(!get(DatabaseKind::Postgres, query).is_empty());
assert!(!get(DatabaseKind::MySql, query).is_empty());
}
}
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[test]
fn boolean_projections_are_normalized_for_sqlx_any() {
// MySQL BOOLEAN is TINYINT(1), which sqlx::Any 0.8 cannot map directly.
for (query, expected_casts) in [
(Query::RESOURCE_EDITOR_SETTINGS_SELECT, 1),
(Query::EDITOR_PREFERENCES_SELECT_PAD, 4),
(Query::EDITOR_PREFERENCES_SELECT_NOTE, 4),
(Query::AUTH_USER_BY_EXTERNAL_ID, 1),
(Query::AUTH_USER_BY_SESSION, 1),
(Query::AUTH_USER_BY_NICKNAME, 1),
(Query::AUTH_USER_BY_EMAIL, 1),
(Query::AUTH_USER_BY_SHARE_IDENTIFIER, 1),
(Query::USER_LIST_WORKSPACES, 6),
(Query::USER_LIST_PADS, 6),
(Query::PAD_PUBLIC_PAGE_DISABLED, 1),
(Query::NOTE_PUBLIC_PAGE_DISABLED, 1),
(Query::Q001, 1),
(Query::Q003, 1),
(Query::Q004, 1),
(Query::Q011, 1),
(Query::Q021, 1),
(Query::Q033, 1),
(Query::Q036, 1),
(Query::Q038, 1),
(Query::Q046, 1),
(Query::Q044, 1),
(Query::Q045, 1),
(Query::Q048, 1),
(Query::Q049, 1),
] {
let sql = get(query);
assert_eq!(
sql.matches("AS SIGNED").count(),
expected_casts,
"MySQL boolean projection is not normalized in {query:?}: {sql}"
);
}
}
+137
View File
@@ -0,0 +1,137 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
fn websocket_headers(origin: &'static str, host: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(header::ORIGIN, HeaderValue::from_static(origin));
headers.insert(header::HOST, HeaderValue::from_static(host));
headers
}
#[test]
fn account_sessions_are_cookie_only() {
let mut headers = HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
HeaderValue::from_static("Bearer legacy-account-token"),
);
assert_eq!(session_token(&headers), None);
assert_eq!(bearer_token(&headers), Some("legacy-account-token"));
headers.insert(
header::COOKIE,
HeaderValue::from_static("__Host-rustpad_session=cookie-token"),
);
assert_eq!(session_token(&headers), Some("cookie-token"));
}
#[test]
fn prefers_proxy_controlled_real_ip() {
let mut headers = HeaderMap::new();
headers.insert(
axum::http::HeaderName::from_static("x-forwarded-for"),
HeaderValue::from_static("203.0.113.10"),
);
headers.insert(
axum::http::HeaderName::from_static("x-real-ip"),
HeaderValue::from_static("198.51.100.20"),
);
assert_eq!(client_key(&headers), "ip:198.51.100.20");
}
#[test]
fn accepts_same_origin_websocket() {
let headers = websocket_headers("https://pad.example.com", "pad.example.com");
assert!(websocket_origin_allowed(&headers));
}
#[test]
fn rejects_cross_origin_websocket() {
let headers = websocket_headers("https://evil.example", "pad.example.com");
assert!(!websocket_origin_allowed(&headers));
}
#[test]
fn does_not_trust_forwarded_host_for_websocket_origin() {
let mut headers = websocket_headers("https://evil.example", "pad.example.com");
headers.insert(
axum::http::HeaderName::from_static("x-forwarded-host"),
HeaderValue::from_static("evil.example"),
);
assert!(!websocket_origin_allowed(&headers));
}
#[test]
fn rejects_origin_with_path() {
let headers = websocket_headers("https://pad.example.com/other", "pad.example.com");
assert!(!websocket_origin_allowed(&headers));
}
#[test]
fn rejects_missing_websocket_origin() {
let mut headers = HeaderMap::new();
headers.insert(header::HOST, HeaderValue::from_static("pad.example.com"));
assert!(!websocket_origin_allowed(&headers));
}
#[test]
fn secure_cookies_are_not_script_readable() {
let value = session_cookie("abc123", 7).to_str().unwrap();
assert!(value.contains("HttpOnly"));
assert!(value.contains("Secure"));
assert!(value.contains("SameSite=Lax"));
assert!(value.starts_with("__Host-rustpad_session=abc123;"));
}
#[test]
fn share_sessions_use_separate_scoped_opaque_cookies() {
let value = share_session_cookie("workspace", "private-space", "opaque", 600)
.to_str()
.unwrap();
assert!(value.starts_with("__Host-rustpad_share_"));
assert!(value.contains("=opaque;"));
assert!(value.contains("Max-Age=600"));
assert!(value.contains("HttpOnly"));
assert!(value.contains("Secure"));
assert!(value.contains("SameSite=Lax"));
}
#[test]
fn csrf_requires_matching_cookie_and_header() {
let token = "a".repeat(CSRF_TOKEN_BYTES * 2);
let mut headers = HeaderMap::new();
headers.insert(
header::COOKIE,
HeaderValue::from_str(&format!("{CSRF_COOKIE}={token}")).unwrap(),
);
headers.insert(
axum::http::HeaderName::from_static(CSRF_HEADER),
HeaderValue::from_str(&token).unwrap(),
);
assert!(csrf_request_is_valid(&headers));
headers.insert(
axum::http::HeaderName::from_static(CSRF_HEADER),
HeaderValue::from_static(
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
),
);
assert!(!csrf_request_is_valid(&headers));
}
#[test]
fn csrf_cookie_is_strict_and_script_readable() {
let token = "a".repeat(CSRF_TOKEN_BYTES * 2);
let value = csrf_cookie(&token).to_str().unwrap();
assert!(value.contains("Secure"));
assert!(value.contains("SameSite=Strict"));
assert!(!value.contains("HttpOnly"));
}
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[test]
fn workspace_password_event_reaches_the_workspace_page_and_its_notes() {
let workspace_key = AppState::workspace_room_key("team");
let note_prefix = "workspace:team/";
assert!(workspace_password_event_channel(
"workspace:team",
&workspace_key,
note_prefix,
));
assert!(workspace_password_event_channel(
"workspace:team/roadmap",
&workspace_key,
note_prefix,
));
assert!(!workspace_password_event_channel(
"workspace:team-two/roadmap",
&workspace_key,
note_prefix,
));
assert!(!workspace_password_event_channel(
"workspace:teams/roadmap",
&workspace_key,
note_prefix,
));
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[test]
fn password_event_only_excludes_the_matching_connection() {
assert!(password_event_excludes_connection(
Some("client_owner"),
"client_owner",
));
assert!(!password_event_excludes_connection(
Some("client_owner"),
"client_visitor",
));
assert!(!password_event_excludes_connection(None, "client_owner"));
}
#[test]
fn password_event_client_id_is_validated() {
assert_eq!(
clean_collaboration_client_id(Some("client_owner_123".into())),
Some("client_owner_123".into()),
);
assert_eq!(clean_collaboration_client_id(Some("short".into())), None);
assert_eq!(
clean_collaboration_client_id(Some("invalid client id!".into())),
None,
);
}
#[test]
fn workspace_watch_authentication_accepts_a_minimal_client_message() {
let message = serde_json::from_str::<WorkspaceWatchClientMessage>(
r#"{"type":"authenticate","access_token":null,"client_id":"workspace_watch_123"}"#,
)
.expect("workspace watch authentication should parse");
match message {
WorkspaceWatchClientMessage::Authenticate {
access_token,
client_id,
} => {
assert!(access_token.is_none());
assert_eq!(client_id.as_deref(), Some("workspace_watch_123"));
}
WorkspaceWatchClientMessage::Ping { .. } => panic!("unexpected ping message"),
}
}