mirror of
https://github.com/we-promise/sure.git
synced 2026-09-07 15:44:21 +00:00
* fix(desktop): support a server hosted under a sub-path `normalize_server_url` rebuilt whatever you typed as `scheme://host[:port]` and dropped the path, so a self-hosted Sure behind a reverse proxy that mounts it under a prefix — `https://home.example.com/sure` — could never be added: the health check hit `{origin}/up`, where something else (or nothing) answers, and the app reported "Couldn't reach a Sure server at that address." The same bare origin was then used for the navigation, the SSO hand-off and the IPC capability grant, so even a passing check would have loaded the wrong site. The path was being dropped for a reason — people paste the URL they have in the clipboard, which is usually a deep link like `/sessions/new`. So rather than guess, let the server say where it is: keep the path in the canonical form, and have `check_server` probe `/up` from the address as typed back up to the origin, returning the first base that answers 200. A root-hosted server still resolves to its origin (one extra candidate, tried only after the specific one 404s), and a transport-level failure returns immediately instead of retrying a host that is not there. `check_server` now answers with that resolved base instead of a bool, and both callers save it rather than the raw input. Tests: the sub-path base, trailing slash / query / fragment trimming, the walk-up candidate list, ports preserved on every candidate, and the cap that still keeps the origin. * fix(desktop): carry the mounted base through SSO and deep links Follow-up to the sub-path support in the previous commit: saving a base with a path fixed adding and loading such a server, but left two flows resolving against the bare origin. The injected bridge only intercepted a form action shaped exactly `/auth/{provider}`, so a mounted server's `/sure/auth/{provider}` was never intercepted, and it emitted `location.origin`, which `begin_sso` would then reject as an unknown server. Match the prefix that precedes `/auth/` and emit the base it implies — empty at a domain root, so root-hosted deployments are unaffected. Deep links were gated on `target.server`, which `deep_link::parse` builds from host and port alone. Gate on the destination instead, and let `is_known_server` accept a URL that a saved base covers. `base_covers` matches only at a path boundary, so a saved `https://sure.example.com` does not cover `https://sure.example.com.evil.test`. Reported by automated review on the PR. * fix(desktop): cap candidates from the deep middle, not the shallow end The cap kept the deepest MAX_BASE_CANDIDATES - 1 candidates plus the origin, which discards the shallow ones in between — and a mount point is shallow. A server mounted at /sure, reached by a link pasted three segments into the app (`/sure/transactions/123/edit`), produced `.../edit`, `.../123`, `/sure/transactions`, origin: `/sure`, the only base that answers, was never probed, so the connection failed with "Couldn't reach a Sure server at that address" against a live server. Keep the address as typed plus the shallowest bases instead. Both readings survive the cap: the base typed exactly, and the mount a deep link sits under. Reported by @jjmata in review. * fix(desktop): bound discovery by mount depth, not candidate count Capping the candidate list drops whole mount depths silently: with a budget of four, `https://host/a/b/c/d/e` never probed `https://host/a/b/c`, so a server mounted there was undiscoverable. Bound the supported mount depth instead. The address as typed is always probed first, so a base entered exactly still works at any depth; the rest are every depth up to MAX_MOUNT_DEPTH, which keeps a pasted deep link finite (typed + 4 probes) without skipping a depth in between. Reported by coderabbitai in review.
125 lines
4.1 KiB
Rust
125 lines
4.1 KiB
Rust
use sure_desktop_lib::servers::{
|
|
base_candidates, base_covers, health_check_url, is_healthy_status, normalize_server_url,
|
|
MAX_MOUNT_DEPTH,
|
|
};
|
|
|
|
#[test]
|
|
fn normalizes_bare_host_to_https_origin() {
|
|
assert_eq!(normalize_server_url("app.example.com").unwrap(), "https://app.example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn preserves_explicit_http_scheme_and_port() {
|
|
assert_eq!(normalize_server_url("http://localhost:3000/").unwrap(), "http://localhost:3000");
|
|
}
|
|
|
|
#[test]
|
|
fn keeps_the_sub_path_a_server_is_mounted_under() {
|
|
assert_eq!(normalize_server_url("https://s.example.com/sure").unwrap(), "https://s.example.com/sure");
|
|
}
|
|
|
|
#[test]
|
|
fn strips_trailing_slash_query_and_fragment() {
|
|
assert_eq!(normalize_server_url("https://s.example.com/sure/").unwrap(), "https://s.example.com/sure");
|
|
assert_eq!(normalize_server_url("https://s.example.com/?a=1#x").unwrap(), "https://s.example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_empty_input() {
|
|
assert!(normalize_server_url(" ").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn builds_health_url() {
|
|
assert_eq!(health_check_url("https://s.example.com"), "https://s.example.com/up");
|
|
assert_eq!(health_check_url("https://s.example.com/sure"), "https://s.example.com/sure/up");
|
|
}
|
|
|
|
#[test]
|
|
fn origin_is_its_own_only_candidate() {
|
|
assert_eq!(base_candidates("https://s.example.com"), vec!["https://s.example.com"]);
|
|
}
|
|
|
|
#[test]
|
|
fn walks_a_pasted_deep_link_back_up_to_the_origin() {
|
|
assert_eq!(
|
|
base_candidates("https://s.example.com/sessions/new"),
|
|
vec![
|
|
"https://s.example.com/sessions/new",
|
|
"https://s.example.com/sessions",
|
|
"https://s.example.com",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn keeps_the_port_on_every_candidate() {
|
|
assert_eq!(
|
|
base_candidates("http://localhost:3000/sure"),
|
|
vec!["http://localhost:3000/sure", "http://localhost:3000"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_base_covers_itself_and_what_sits_under_it() {
|
|
assert!(base_covers("https://s.example.com", "https://s.example.com"));
|
|
assert!(base_covers("https://s.example.com", "https://s.example.com/accounts"));
|
|
assert!(base_covers("https://s.example.com/sure", "https://s.example.com/sure/accounts"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_base_does_not_cover_a_lookalike_host_or_a_sibling_path() {
|
|
assert!(!base_covers("https://s.example.com", "https://s.example.com.evil.test/accounts"));
|
|
assert!(!base_covers("https://s.example.com/sure", "https://s.example.com/surely"));
|
|
assert!(!base_covers("https://s.example.com/sure", "https://s.example.com"));
|
|
}
|
|
|
|
// A deep link is probed against the address as typed and against every mount
|
|
// depth up to MAX_MOUNT_DEPTH — no supported mount is skipped, at any depth.
|
|
#[test]
|
|
fn probes_the_typed_address_and_every_supported_mount_depth() {
|
|
assert_eq!(
|
|
base_candidates("https://s.example.com/a/b/c/d/e"),
|
|
vec![
|
|
"https://s.example.com/a/b/c/d/e",
|
|
"https://s.example.com/a/b/c",
|
|
"https://s.example.com/a/b",
|
|
"https://s.example.com/a",
|
|
"https://s.example.com",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn probes_the_shallow_mount_a_deep_link_sits_under() {
|
|
let candidates = base_candidates("https://s.example.com/sure/transactions/123/edit");
|
|
assert!(
|
|
candidates.contains(&"https://s.example.com/sure".to_string()),
|
|
"the mount base was dropped: {candidates:?}"
|
|
);
|
|
assert_eq!(candidates[0], "https://s.example.com/sure/transactions/123/edit");
|
|
assert_eq!(candidates.last().unwrap(), "https://s.example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn a_multi_segment_mount_is_reached_through_a_deep_link() {
|
|
let candidates = base_candidates("https://s.example.com/apps/finance/sure/accounts/42");
|
|
assert!(
|
|
candidates.contains(&"https://s.example.com/apps/finance/sure".to_string()),
|
|
"a {MAX_MOUNT_DEPTH}-segment mount was dropped: {candidates:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_walk_stays_bounded() {
|
|
let deep = format!("https://s.example.com/{}", vec!["seg"; 40].join("/"));
|
|
assert_eq!(base_candidates(&deep).len(), MAX_MOUNT_DEPTH + 2); // typed + depths 3..0
|
|
}
|
|
|
|
#[test]
|
|
fn only_200_is_healthy() {
|
|
assert!(is_healthy_status(200));
|
|
assert!(!is_healthy_status(302));
|
|
assert!(!is_healthy_status(500));
|
|
}
|