improvements
This commit is contained in:
@@ -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));
|
||||
}
|
||||
@@ -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(
|
||||
"",
|
||||
"image.png",
|
||||
stored,
|
||||
None,
|
||||
));
|
||||
assert!(content_references_stored_file(
|
||||
"",
|
||||
"image.png",
|
||||
"https://old-files.example.com/f/token/image.png",
|
||||
Some("https://new-files.example.com"),
|
||||
));
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user