Chat+ link previews: a link in a chat shows its page's title, text and picture, YouTube playable in place
X's web chat shows a link as a bare card. An X tab on a chat page may now ask native code (link_preview, x-capture only, chat pages only) for one link's preview. Native code fetches it: https and public hosts only (addresses checked and pinned), never X's hosts, no cookies or referrer, redirects re-checked, page head <= 512 KB, images <= 1.5 MB by magic bytes as data:, 30 fetches a minute, memory-only cache. Only the link leaves the page. On by default (owner, 2026-10-10).
This commit is contained in:
parent
7530fb1de7
commit
6d1798038c
21 changed files with 1455 additions and 519 deletions
|
|
@ -1,430 +1,434 @@
|
|||
use url::Url;
|
||||
use better_twttr::{policy::*, site::SITE, theme::{x_default, XTheme, X_THEMES}};
|
||||
|
||||
#[test]
|
||||
fn exact_origin_and_window_scope_is_enforced() {
|
||||
let x = Url::parse("https://x.com/home").unwrap();
|
||||
assert!(require_capture("tab-1", &x).is_ok());
|
||||
assert!(require_capture("shell", &x).is_err());
|
||||
assert!(require_local(
|
||||
"tab-1",
|
||||
&Url::parse("tauri://localhost/index.html").unwrap(),
|
||||
false
|
||||
)
|
||||
.is_err());
|
||||
for value in [
|
||||
"https://evil.x.com/home",
|
||||
"http://x.com/home",
|
||||
"https://x.com:8443/home",
|
||||
"https://x.com.evil.test/home",
|
||||
"https://x.com@evil.test/home",
|
||||
"https://evil@x.com/home",
|
||||
"file:///tmp/index.html",
|
||||
"https://twitter.com/home",
|
||||
] {
|
||||
assert!(
|
||||
require_capture("tab-1", &Url::parse(value).unwrap()).is_err(),
|
||||
"accepted {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_origin_has_no_production_localhost_backdoor() {
|
||||
for value in [
|
||||
"tauri://localhost/index.html",
|
||||
"http://tauri.localhost/index.html",
|
||||
"https://tauri.localhost/index.html",
|
||||
] {
|
||||
assert!(require_local("shell", &Url::parse(value).unwrap(), false).is_ok());
|
||||
}
|
||||
assert!(require_local("shell", &Url::parse("http://127.0.0.1:1420/").unwrap(), true).is_ok());
|
||||
for value in [
|
||||
"https://x.com/",
|
||||
"http://127.0.0.1:1420/",
|
||||
"http://localhost:1420/",
|
||||
"https://tauri.localhost.evil.test/",
|
||||
"tauri://evil/",
|
||||
] {
|
||||
assert!(
|
||||
require_local("shell", &Url::parse(value).unwrap(), false).is_err(),
|
||||
"accepted {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_allows_only_x_and_specific_legacy_login_routes() {
|
||||
for value in [
|
||||
"https://x.com/home",
|
||||
"https://twitter.com/login",
|
||||
"https://twitter.com/i/flow/login",
|
||||
"https://twitter.com/i/flow/login/step",
|
||||
] {
|
||||
assert!(x_navigation_allowed(&Url::parse(value).unwrap()));
|
||||
}
|
||||
for value in [
|
||||
"https://twitter.com/home",
|
||||
"https://t.co/test",
|
||||
"https://accounts.google.com/",
|
||||
"javascript:alert(1)",
|
||||
"mailto:test@example.com",
|
||||
"https://twitter.com/login/evil",
|
||||
] {
|
||||
assert!(
|
||||
!x_navigation_allowed(&Url::parse(value).unwrap()),
|
||||
"accepted {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_capabilities_cannot_reach_the_vault() {
|
||||
let read = |json: &str| serde_json::from_str::<serde_json::Value>(json).unwrap();
|
||||
let x = read(include_str!("../capabilities/x-capture.json"));
|
||||
// Drafts popover (2026-09-27): X may only open/close it about itself and ask one yes/no hint question.
|
||||
assert_eq!(x["permissions"], serde_json::json!(["allow-capture-draft", "allow-open-panel", "allow-drafts-picker-open", "allow-drafts-popover-dismiss", "allow-drafts-hint", "allow-keyword-alert"]));
|
||||
for vault in ["allow-popover-drafts", "allow-popover-insert", "allow-popover-state", "allow-draft-list"] {
|
||||
assert!(!x["permissions"].as_array().unwrap().iter().any(|p| p == vault), "X tabs have {vault}");
|
||||
}
|
||||
|
||||
// The drafts popover is local-only and gets nothing beyond its own five commands.
|
||||
let popover = read(include_str!("../capabilities/drafts-popover.json"));
|
||||
assert_eq!(popover["webviews"], serde_json::json!(["popover-*"]));
|
||||
assert_eq!(popover["local"], true);
|
||||
assert!(popover.get("remote").is_none());
|
||||
assert_eq!(popover["permissions"], serde_json::json!(["allow-popover-state", "allow-popover-drafts", "allow-popover-insert", "allow-popover-close", "allow-popover-open-vault"]));
|
||||
assert_eq!(x["webviews"], serde_json::json!(["tab-*"]));
|
||||
assert_eq!(x["remote"]["urls"], serde_json::json!(["https://x.com/*"]));
|
||||
assert_eq!(x["local"], false);
|
||||
// Linux joined with the same permissions (owner request, 2026-09-28): platforms only, no new grants.
|
||||
assert_eq!(x["platforms"], serde_json::json!(["macOS", "windows", "linux"]));
|
||||
|
||||
let tabs = read(include_str!("../capabilities/tab-chrome.json"));
|
||||
assert_eq!(tabs["permissions"], serde_json::json!(["allow-tab-report", "allow-tab-shortcut", "allow-tab-link-drag"]));
|
||||
assert_eq!(tabs["webviews"], serde_json::json!(["tab-*"]));
|
||||
assert_eq!(tabs["local"], false);
|
||||
|
||||
let local = read(include_str!("../capabilities/workspace-local.json"));
|
||||
assert!(local.get("remote").is_none());
|
||||
// My Space (profile customization) is removed: its commands do not exist, so no capability
|
||||
// file grants them and neither the command manifest nor the handler names them.
|
||||
let (build, desktop) = (include_str!("../build.rs"), include_str!("../src/desktop.rs"));
|
||||
for gone in [
|
||||
"profile_owner_get", "profile_create", "profile_api", "profile_media_pick", "profile_media_still", "profile_media_upload", "profile_still_to_x", "profile_view_get", "profile_view_set",
|
||||
"bt_profiles_resolve", "bt_media_get", "bt_profile_link_seen",
|
||||
] {
|
||||
let permission = format!("allow-{}", gone.replace('_', "-"));
|
||||
for (name, capability) in [("x-capture", &x), ("drafts-popover", &popover), ("tab-chrome", &tabs), ("workspace-local", &local)] {
|
||||
assert!(!capability["permissions"].as_array().unwrap().iter().any(|p| p == permission.as_str()), "{name} grants {permission}");
|
||||
}
|
||||
assert!(!build.contains(&format!("\"{gone}\"")), "build.rs still lists {gone}");
|
||||
assert!(!desktop.contains(gone), "desktop.rs still names {gone}");
|
||||
}
|
||||
assert_eq!(local["webviews"], serde_json::json!(["shell"]));
|
||||
|
||||
// Update commands are shell-only; no capability exposes the updater/process plugin JS APIs.
|
||||
let perms = |v: &serde_json::Value| v["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_owned()).collect::<Vec<_>>();
|
||||
// Account profiles (separate X logins) are managed only by the shell.
|
||||
// Drafts are inserted and alert keywords edited only by the shell; X tabs only ask/report.
|
||||
for name in [
|
||||
"allow-update-status", "allow-apply-update", "allow-app-ready", "allow-profiles-get", "allow-profiles-set",
|
||||
"allow-tab-insert-text", "allow-alerts-get", "allow-alerts-set", "allow-draft-list", "allow-draft-revisions",
|
||||
// Google Fonts downloads (network), account/pane themes and the builder preview: shell only.
|
||||
"allow-gfonts-installed", "allow-gfonts-install", "allow-gfonts-remove", "allow-gfonts-faces", "allow-gfonts-preview",
|
||||
"allow-theme-scopes-get", "allow-theme-scopes-set", "allow-theme-preview",
|
||||
// Installing themes/plugins (links, dropped files, the signed store; owner-approved 2026-09-28):
|
||||
// only the shell, after its confirmation screen. X tabs and web pages cannot install anything.
|
||||
"allow-install-pending", "allow-install-stage-link", "allow-install-stage-file", "allow-install-cancel",
|
||||
"allow-install-commit", "allow-install-records", "allow-install-forget", "allow-plugin-remove",
|
||||
"allow-store-catalog", "allow-store-stage",
|
||||
] {
|
||||
assert!(perms(&local).iter().any(|p| p == name), "shell lacks {name}");
|
||||
assert!(!perms(&x).iter().chain(perms(&tabs).iter()).any(|p| p == name), "tab has {name}");
|
||||
}
|
||||
for p in perms(&local).iter().chain(perms(&x).iter()).chain(perms(&tabs).iter()) {
|
||||
assert!(
|
||||
!p.starts_with("updater:") && !p.starts_with("process:") && !p.starts_with("notification:") && !p.starts_with("deep-link:"),
|
||||
"plugin API exposed: {p}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_accepts_only_known_presets_and_hex_accents() {
|
||||
let script = |theme: XTheme| theme.script(&X_THEMES, &SITE);
|
||||
let valid = XTheme { preset: "night-ink".into(), accent: Some("#1d9bf0".into()), ..x_default() };
|
||||
let text = script(valid).unwrap();
|
||||
assert!(text.contains("location.origin!=='https://x.com'"));
|
||||
assert!(text.contains(r#""preset":"night-ink""#));
|
||||
assert!(x_default().is_default(&X_THEMES));
|
||||
for preset in ["x", "classic", "night-ink", "terminal", "paper"] {
|
||||
assert!(script(XTheme { preset: preset.into(), ..x_default() }).is_ok());
|
||||
}
|
||||
for hostile in ["evil", "x;display:none", "</script>", ""] {
|
||||
assert!(script(XTheme { preset: hostile.into(), ..x_default() }).is_err(), "accepted {hostile}");
|
||||
}
|
||||
for hostile in ["red", "#fff", "url(https://evil.test)", "#000000;display:none", "</style><script>", "#00000é"] {
|
||||
assert!(script(XTheme { accent: Some(hostile.into()), ..x_default() }).is_err(), "accepted {hostile}");
|
||||
}
|
||||
assert!(serde_json::from_str::<XTheme>(r#"{"preset":"x","css":"body{}"}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_keeps_macos_targets_and_signed_updater() {
|
||||
let conf: serde_json::Value = serde_json::from_str(include_str!("../tauri.conf.json")).unwrap();
|
||||
let targets = conf["bundle"]["targets"].as_array().unwrap();
|
||||
for target in ["nsis", "app", "dmg"] {
|
||||
assert!(targets.iter().any(|t| t == target), "missing bundle target {target}");
|
||||
}
|
||||
assert!(!conf["plugins"]["updater"]["pubkey"].as_str().unwrap_or("").is_empty());
|
||||
for endpoint in conf["plugins"]["updater"]["endpoints"].as_array().unwrap() {
|
||||
assert!(endpoint.as_str().unwrap().starts_with("https://"), "updater endpoint must be https");
|
||||
}
|
||||
// One URL scheme, only for install links; the installer/Info.plist register exactly this.
|
||||
assert_eq!(conf["plugins"]["deep-link"]["desktop"]["schemes"], serde_json::json!(["waybetter"]));
|
||||
assert!(conf["plugins"]["deep-link"].get("mobile").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backup_sync_workspace_palette_and_dev_commands_are_shell_only() {
|
||||
let read = |json: &str| serde_json::from_str::<serde_json::Value>(json).unwrap();
|
||||
let perms = |v: &serde_json::Value| v["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_owned()).collect::<Vec<_>>();
|
||||
let local = perms(&read(include_str!("../capabilities/workspace-local.json")));
|
||||
let x = perms(&read(include_str!("../capabilities/x-capture.json")));
|
||||
let tabs = perms(&read(include_str!("../capabilities/tab-chrome.json")));
|
||||
let popover = perms(&read(include_str!("../capabilities/drafts-popover.json")));
|
||||
// Settings backup/sync (files and the sync key stay native), workspaces, running a plugin's
|
||||
// palette command in a tab, and developer mode (file watcher, hot reload).
|
||||
for name in [
|
||||
"allow-tab-run-command", "allow-workspaces-get", "allow-workspaces-set",
|
||||
"allow-backup-collect", "allow-backup-export-file", "allow-backup-open-file", "allow-backup-apply",
|
||||
"allow-sync-status", "allow-sync-choose-folder", "allow-sync-unlock", "allow-sync-lock", "allow-sync-disable",
|
||||
"allow-sync-configure", "allow-sync-push", "allow-sync-check", "allow-sync-open", "allow-sync-mark-pulled",
|
||||
"allow-dev-get", "allow-dev-set", "allow-dev-pick-theme", "allow-dev-clear-theme", "allow-dev-reload-plugin", "allow-dev-report-error",
|
||||
] {
|
||||
assert!(local.iter().any(|p| p == name), "shell lacks {name}");
|
||||
assert!(!x.iter().chain(tabs.iter()).chain(popover.iter()).any(|p| p == name), "a tab or the popover has {name}");
|
||||
}
|
||||
// X tabs report their plugins' commands only through tab_report (owner-approved 2026-09-28);
|
||||
// the tab capabilities gained nothing for it.
|
||||
assert_eq!(tabs, ["allow-tab-report", "allow-tab-shortcut", "allow-tab-link-drag"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_commands_are_shell_only() {
|
||||
let read = |json: &str| serde_json::from_str::<serde_json::Value>(json).unwrap();
|
||||
let perms = |v: &serde_json::Value| v["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_owned()).collect::<Vec<_>>();
|
||||
let local = perms(&read(include_str!("../capabilities/workspace-local.json")));
|
||||
let others: Vec<String> = [
|
||||
include_str!("../capabilities/x-capture.json"),
|
||||
include_str!("../capabilities/tab-chrome.json"),
|
||||
include_str!("../capabilities/drafts-popover.json"),
|
||||
]
|
||||
.iter()
|
||||
.flat_map(|json| perms(&read(json)))
|
||||
.collect();
|
||||
// The shell picks the language and hands native code and X tabs their strings; tabs and the
|
||||
// popover only ever receive them (X tabs as data through the runtime), never set them.
|
||||
for command in ["allow-i18n-get", "allow-i18n-set"] {
|
||||
assert!(local.iter().any(|p| p == command), "shell lacks {command}");
|
||||
assert!(!others.iter().any(|p| p == command), "a tab or the popover has {command}");
|
||||
}
|
||||
// The commands are the framework's (waybetter-host `app`); each checks shell_only first.
|
||||
for command in ["i18n_get", "i18n_set"] {
|
||||
waybetter_host::testkit::assert_guarded(include_str!("../../../../waybetter/crates/waybetter-host/src/app.rs"), command, "shell_only(&webview)?");
|
||||
}
|
||||
// X tabs get the strings only as a guarded data value, and only the runtime's own prefixes.
|
||||
let source = include_str!("../src/desktop_i18n.rs");
|
||||
assert!(source.contains(r#"&["x.", "kit.", "plugin."]"#), "X tabs receive only x./kit./plugin. strings");
|
||||
assert!(include_str!("../src/desktop.rs").contains("page_string_prefixes: crate::desktop_i18n::PAGE_PREFIXES"), "X's config hands tabs exactly those");
|
||||
let strings = waybetter_host::i18n::NativeStrings {
|
||||
locale: "tr".into(),
|
||||
preference: "site".into(),
|
||||
messages: [("x.fab.title", "wayBetter"), ("kit.ok", "Tamam"), ("native.notify.alert.title", "Uyarı")]
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.to_owned(), value.to_owned()))
|
||||
.collect(),
|
||||
};
|
||||
let script = waybetter_host::i18n::page_script(&better_twttr::site::SITE, &strings.page_payload(&["x.", "kit.", "plugin."]));
|
||||
assert!(script.starts_with("if(location.origin==='https://x.com')("), "page strings must be x.com only: {script}");
|
||||
assert!(script.contains("x.fab.title") && script.contains("kit.ok") && !script.contains("native.notify"), "{script}");
|
||||
// "Follow X": a tab may report X's <html lang>, only as a valid tag and only while on x.com.
|
||||
let desktop = include_str!("../src/desktop.rs");
|
||||
assert!(
|
||||
desktop.contains("report.lang.filter(|lang| waybetter_host::i18n::valid_tag(lang) && webview.url().is_ok_and(|url| policy::x_origin(&url)))"),
|
||||
"tab_report lang must be a validated tag from a webview on x.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_strings_are_plain_text() {
|
||||
use waybetter_host::i18n::NativeStrings;
|
||||
let set = |value: &str| NativeStrings {
|
||||
locale: "ar".into(),
|
||||
preference: "site".into(),
|
||||
messages: [("x.fab.title".to_owned(), value.to_owned())].into_iter().collect(),
|
||||
};
|
||||
assert!(set("wayBetter لـ X").validated().is_ok());
|
||||
for bad in ["<img src=x onerror=alert(1)>", "<script>", "javascript:alert(1)", "a\u{202e}b"] {
|
||||
assert!(set(bad).validated().is_err(), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_raise_is_shell_only() {
|
||||
let read = |json: &str| serde_json::from_str::<serde_json::Value>(json).unwrap();
|
||||
let perms = |v: &serde_json::Value| v["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_owned()).collect::<Vec<_>>();
|
||||
let local = perms(&read(include_str!("../capabilities/workspace-local.json")));
|
||||
let x = perms(&read(include_str!("../capabilities/x-capture.json")));
|
||||
let tabs = perms(&read(include_str!("../capabilities/tab-chrome.json")));
|
||||
let popover = perms(&read(include_str!("../capabilities/drafts-popover.json")));
|
||||
// Re-orders the shell webview over the tabs for its own overlays: only the shell may.
|
||||
assert!(local.iter().any(|p| p == "allow-shell-raise"), "shell lacks allow-shell-raise");
|
||||
assert!(!x.iter().chain(tabs.iter()).chain(popover.iter()).any(|p| p == "allow-shell-raise"), "a tab or the popover can raise the shell");
|
||||
// The command is the framework's (waybetter-host `app`).
|
||||
waybetter_host::testkit::assert_guarded(include_str!("../../../../waybetter/crates/waybetter-host/src/app.rs"), "shell_raise", "shell_only(&webview)?");
|
||||
waybetter_host::testkit::Capabilities::from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/capabilities")).assert_shell_only("shell_raise");
|
||||
}
|
||||
|
||||
/// The wayBetter host's own commands (tab layout and actions, shell raise, external links, window
|
||||
/// menu, profiles, language; tab shortcuts and link drags) keep their callers in X's capability
|
||||
/// files, and every command wrapper checks its caller first. Every capability file is read, so a
|
||||
/// new one cannot quietly widen them.
|
||||
#[test]
|
||||
fn host_commands_keep_their_callers() {
|
||||
use waybetter_host::testkit::{assert_guarded, assert_host_guards, Capabilities};
|
||||
let caps = Capabilities::from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/capabilities"));
|
||||
caps.assert_host_commands();
|
||||
assert_host_guards();
|
||||
// Only Rust drives the plugins; no capability exposes their JS APIs.
|
||||
caps.assert_no_plugin_apis(&["updater", "process", "notification", "deep-link", "single-instance", "window-state", "opener", "dialog"]);
|
||||
// X's own: vault, settings, themes, installs, backups, sync, dev mode, updates are the shell's.
|
||||
for command in [
|
||||
"draft_list", "draft_save", "draft_revisions", "draft_status", "observation_list", "data_export",
|
||||
"gfonts_installed", "gfonts_install", "gfonts_remove", "gfonts_faces", "gfonts_preview",
|
||||
"tab_run_command", "workspaces_get", "workspaces_set", "backup_collect", "backup_export_file", "backup_open_file", "backup_apply",
|
||||
"sync_status", "sync_choose_folder", "sync_unlock", "sync_lock", "sync_disable", "sync_configure", "sync_push", "sync_check", "sync_open", "sync_mark_pulled",
|
||||
"dev_get", "dev_set", "dev_pick_theme", "dev_clear_theme", "dev_reload_plugin", "dev_report_error",
|
||||
"plugins_list", "plugin_set_enabled", "plugins_open_folder", "plugin_remove",
|
||||
"install_pending", "install_stage_link", "install_stage_file", "install_stage_opened", "install_cancel", "install_commit", "install_records", "install_forget", "store_catalog", "store_stage",
|
||||
"custom_css_get", "custom_css_set", "features_get", "features_set", "mode_get", "mode_set", "settings_get", "settings_set", "column_configure",
|
||||
"update_status", "update_check", "apply_update", "app_ready", "tab_insert_text", "alerts_get", "alerts_set",
|
||||
"media_get", "media_set", "gallery_add_folder", "gallery_list", "gallery_thumb", "clip_list", "clip_thumb", "clip_remove", "tab_attach_image",
|
||||
] {
|
||||
caps.assert_shell_only(command);
|
||||
}
|
||||
// Tabs, about themselves only; the X-only ones are granted only on https://x.com.
|
||||
for command in ["tab_report", "capture_draft", "open_panel", "drafts_picker_open", "drafts_popover_dismiss", "drafts_hint", "keyword_alert"] {
|
||||
caps.assert_tab_only(command);
|
||||
}
|
||||
let desktop = include_str!("../src/desktop.rs");
|
||||
assert_guarded(desktop, "tab_insert_text", "shell_only(&webview)?");
|
||||
// The vault commands are the host's: shell-only in the capability files and each checking its caller
|
||||
// first; X's own handler no longer has them, and X turns the vault on with its rules.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::VAULT);
|
||||
waybetter_host::testkit::assert_vault_guards();
|
||||
assert!(desktop.contains(".with_vault("), "the app enables the host's vault");
|
||||
for command in waybetter_host::commands::VAULT {
|
||||
assert!(!desktop.contains(&format!("fn {command}(")), "{command} is served by the host only");
|
||||
}
|
||||
// The updater commands are the shell's too, each checking its caller first; `update_check` runs the
|
||||
// timer's own check once (no other endpoint), and tabs have no updater command at all.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::UPDATE);
|
||||
waybetter_host::testkit::assert_update_guards();
|
||||
assert!(desktop.contains(".with_updates("), "the app enables the updater");
|
||||
// Plugins, custom CSS, the tweak flags and the browser settings are served by the host: they
|
||||
// are in its shell-only list (checked against the capability files and for their caller
|
||||
// check above), and X's own handler no longer has them.
|
||||
let host = include_str!("../../../../waybetter/crates/waybetter-host/src/app.rs");
|
||||
for command in ["plugins_list", "plugin_set_enabled", "plugins_open_folder", "custom_css_get", "custom_css_set", "features_get", "features_set", "mode_get", "mode_set", "settings_get", "settings_set"] {
|
||||
assert!(waybetter_host::commands::SHELL.contains(&command), "{command} is a host shell-only command");
|
||||
assert_guarded(host, command, "shell_only(&webview)?");
|
||||
assert!(!desktop.contains(&format!("fn {command}(")), "{command} is served by the host only");
|
||||
}
|
||||
assert_guarded(desktop, "tab_report", "tab_only(&webview)?");
|
||||
// Opening the panel from a page is the host's site command: only X tabs on x.com have it.
|
||||
caps.assert_site_only("open_panel", &better_twttr::site::SITE);
|
||||
assert!(!desktop.contains("fn open_panel("), "open_panel is served by the host only");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_store_media_and_font_commands_are_shell_only() {
|
||||
// The store's commands are the framework's (waybetter-host `store_commands`, served because
|
||||
// this app sets `AppConfig::store`): shell-only in the capability files, and each wrapper
|
||||
// checks shell_only before anything else.
|
||||
use waybetter_host::testkit::assert_guarded;
|
||||
let caps = waybetter_host::testkit::Capabilities::from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/capabilities"));
|
||||
caps.assert_shell_only_group(waybetter_host::commands::STORE);
|
||||
waybetter_host::testkit::assert_store_guards();
|
||||
// The media panel's commands are the framework's too (AppConfig::with_media): shell-only, each
|
||||
// checking its caller first; only attaching to X's composer is the app's own.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::MEDIA);
|
||||
waybetter_host::testkit::assert_media_guards();
|
||||
// Workspaces and encrypted sync too (AppConfig::with_sync; workspaces are served for every app).
|
||||
caps.assert_shell_only_group(waybetter_host::commands::WORKSPACES);
|
||||
caps.assert_shell_only_group(waybetter_host::commands::SYNC);
|
||||
waybetter_host::testkit::assert_sync_guards();
|
||||
// The settings backup's commands are the framework's too (what it carries is the app's hooks).
|
||||
caps.assert_shell_only_group(waybetter_host::commands::BACKUP);
|
||||
waybetter_host::testkit::assert_backup_guards();
|
||||
// Developer mode (AppConfig::with_dev): the framework's commands; the theme file is the app's hook.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::DEV);
|
||||
waybetter_host::testkit::assert_dev_guards();
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_dev()"), "the app enables developer mode");
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_sync()"), "the app enables sync");
|
||||
// Site themes too (AppConfig::with_themes): the framework's commands; X only names its built-in themes.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::THEME);
|
||||
waybetter_host::testkit::assert_theme_guards();
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_themes(&crate::theme::THEMES)"), "the app configures its themes");
|
||||
// The keyword alert settings (AppConfig::with_alerts): the framework's commands; the hit and its notification are X's.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::ALERTS);
|
||||
waybetter_host::testkit::assert_alerts_guards();
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_alerts(&alerts::ALERTS)"), "the app configures its alerts");
|
||||
// Google Fonts too (AppConfig::with_fonts).
|
||||
caps.assert_shell_only_group(waybetter_host::commands::GFONTS);
|
||||
waybetter_host::testkit::assert_fonts_guards();
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_fonts()"), "the app enables Google Fonts");
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_media()"), "the app enables the media panel");
|
||||
assert_guarded(include_str!("../src/desktop_media.rs"), "tab_attach_image", "shell_only(&webview)?");
|
||||
let source = include_str!("../../../../waybetter/crates/waybetter-host/src/store_commands.rs");
|
||||
// Links only ever propose an install: the deep-link handler queues and emits, nothing more.
|
||||
let received = &source[source.find("fn received(").unwrap()..];
|
||||
let received = &received[..received.find("\n}").unwrap()];
|
||||
assert!(!received.contains("commit") && !received.contains("stage"), "a link must not stage or install by itself");
|
||||
// The app's store settings: X's id and the framework's hosts; the compiled-in key; and it asks
|
||||
// the framework to serve the store.
|
||||
let settings = include_str!("../src/distribution.rs");
|
||||
assert!(settings.contains("app_id: \"x\"") && settings.contains("allowed_hosts: DEFAULT_HOSTS") && settings.contains("include_str!(\"../keys/store.pub\")"));
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_store(&crate::distribution::STORE)"), "the app configures the store");
|
||||
}
|
||||
|
||||
/// The popover's webview and session are the host's; X's commands reached by the popover each check
|
||||
/// their caller first (the popover webview with the open session), and inserting a draft only
|
||||
/// focuses the tab and types (`compose` scripts), never clicks Post.
|
||||
#[test]
|
||||
fn popover_commands_check_their_caller_and_never_post() {
|
||||
use waybetter_host::testkit::assert_guarded;
|
||||
let source = include_str!("../src/desktop_popover.rs");
|
||||
assert_guarded(source, "popover_state", "host::session_of(&webview)");
|
||||
for command in ["popover_drafts", "popover_insert", "popover_open_vault"] {
|
||||
assert_guarded(source, command, "host::current(&webview,");
|
||||
}
|
||||
// popover_close only acts for the current popover, and does nothing otherwise.
|
||||
let close = &source[source.find("fn popover_close(").unwrap()..];
|
||||
let close = &close[..close.find("
|
||||
}").unwrap()];
|
||||
assert!(close.contains("host::current(&webview, seq).is_ok()"));
|
||||
let insert = &source[source.find("fn popover_insert(").unwrap()..];
|
||||
let insert = &insert[..insert.find("
|
||||
}").unwrap()];
|
||||
assert!(insert.contains("compose::insert_script") && insert.contains("compose::append_script"));
|
||||
assert!(!insert.contains("click") && !insert.contains("submit") && !insert.contains("Post"), "popover_insert never posts");
|
||||
assert!(insert.contains("policy::x_origin(&url)"), "only into X tabs");
|
||||
// The host's popover check: only a `popover-<n>` webview on the app's own local origin.
|
||||
let host = include_str!("../../../../waybetter/crates/waybetter-host/src/popover_host.rs");
|
||||
let only = &host[host.find("pub fn popover_only(").unwrap()..];
|
||||
let only = &only[..only.find("
|
||||
}").unwrap()];
|
||||
assert!(only.contains("popover::valid_label(webview.label())") && only.contains("local_navigation_allowed"));
|
||||
}
|
||||
use url::Url;
|
||||
use better_twttr::{policy::*, site::SITE, theme::{x_default, XTheme, X_THEMES}};
|
||||
|
||||
#[test]
|
||||
fn exact_origin_and_window_scope_is_enforced() {
|
||||
let x = Url::parse("https://x.com/home").unwrap();
|
||||
assert!(require_capture("tab-1", &x).is_ok());
|
||||
assert!(require_capture("shell", &x).is_err());
|
||||
assert!(require_local(
|
||||
"tab-1",
|
||||
&Url::parse("tauri://localhost/index.html").unwrap(),
|
||||
false
|
||||
)
|
||||
.is_err());
|
||||
for value in [
|
||||
"https://evil.x.com/home",
|
||||
"http://x.com/home",
|
||||
"https://x.com:8443/home",
|
||||
"https://x.com.evil.test/home",
|
||||
"https://x.com@evil.test/home",
|
||||
"https://evil@x.com/home",
|
||||
"file:///tmp/index.html",
|
||||
"https://twitter.com/home",
|
||||
] {
|
||||
assert!(
|
||||
require_capture("tab-1", &Url::parse(value).unwrap()).is_err(),
|
||||
"accepted {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_origin_has_no_production_localhost_backdoor() {
|
||||
for value in [
|
||||
"tauri://localhost/index.html",
|
||||
"http://tauri.localhost/index.html",
|
||||
"https://tauri.localhost/index.html",
|
||||
] {
|
||||
assert!(require_local("shell", &Url::parse(value).unwrap(), false).is_ok());
|
||||
}
|
||||
assert!(require_local("shell", &Url::parse("http://127.0.0.1:1420/").unwrap(), true).is_ok());
|
||||
for value in [
|
||||
"https://x.com/",
|
||||
"http://127.0.0.1:1420/",
|
||||
"http://localhost:1420/",
|
||||
"https://tauri.localhost.evil.test/",
|
||||
"tauri://evil/",
|
||||
] {
|
||||
assert!(
|
||||
require_local("shell", &Url::parse(value).unwrap(), false).is_err(),
|
||||
"accepted {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_allows_only_x_and_specific_legacy_login_routes() {
|
||||
for value in [
|
||||
"https://x.com/home",
|
||||
"https://twitter.com/login",
|
||||
"https://twitter.com/i/flow/login",
|
||||
"https://twitter.com/i/flow/login/step",
|
||||
] {
|
||||
assert!(x_navigation_allowed(&Url::parse(value).unwrap()));
|
||||
}
|
||||
for value in [
|
||||
"https://twitter.com/home",
|
||||
"https://t.co/test",
|
||||
"https://accounts.google.com/",
|
||||
"javascript:alert(1)",
|
||||
"mailto:test@example.com",
|
||||
"https://twitter.com/login/evil",
|
||||
] {
|
||||
assert!(
|
||||
!x_navigation_allowed(&Url::parse(value).unwrap()),
|
||||
"accepted {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_capabilities_cannot_reach_the_vault() {
|
||||
let read = |json: &str| serde_json::from_str::<serde_json::Value>(json).unwrap();
|
||||
let x = read(include_str!("../capabilities/x-capture.json"));
|
||||
// Drafts popover (2026-09-27): X may only open/close it about itself and ask one yes/no hint question.
|
||||
// Chat+ link previews (owner-approved, 2026-10-10): one link at a time, from a chat page only.
|
||||
assert_eq!(x["permissions"], serde_json::json!(["allow-capture-draft", "allow-open-panel", "allow-drafts-picker-open", "allow-drafts-popover-dismiss", "allow-drafts-hint", "allow-keyword-alert", "allow-link-preview"]));
|
||||
let desktop = include_str!("../src/desktop.rs");
|
||||
waybetter_host::testkit::assert_guarded(desktop, "link_preview", "policy::require_capture(webview.label(), &webview.url()");
|
||||
assert!(desktop.contains("link_preview::is_chat_page(page.path())"), "link previews answer chat pages only");
|
||||
for vault in ["allow-popover-drafts", "allow-popover-insert", "allow-popover-state", "allow-draft-list"] {
|
||||
assert!(!x["permissions"].as_array().unwrap().iter().any(|p| p == vault), "X tabs have {vault}");
|
||||
}
|
||||
|
||||
// The drafts popover is local-only and gets nothing beyond its own five commands.
|
||||
let popover = read(include_str!("../capabilities/drafts-popover.json"));
|
||||
assert_eq!(popover["webviews"], serde_json::json!(["popover-*"]));
|
||||
assert_eq!(popover["local"], true);
|
||||
assert!(popover.get("remote").is_none());
|
||||
assert_eq!(popover["permissions"], serde_json::json!(["allow-popover-state", "allow-popover-drafts", "allow-popover-insert", "allow-popover-close", "allow-popover-open-vault"]));
|
||||
assert_eq!(x["webviews"], serde_json::json!(["tab-*"]));
|
||||
assert_eq!(x["remote"]["urls"], serde_json::json!(["https://x.com/*"]));
|
||||
assert_eq!(x["local"], false);
|
||||
// Linux joined with the same permissions (owner request, 2026-09-28): platforms only, no new grants.
|
||||
assert_eq!(x["platforms"], serde_json::json!(["macOS", "windows", "linux"]));
|
||||
|
||||
let tabs = read(include_str!("../capabilities/tab-chrome.json"));
|
||||
assert_eq!(tabs["permissions"], serde_json::json!(["allow-tab-report", "allow-tab-shortcut", "allow-tab-link-drag"]));
|
||||
assert_eq!(tabs["webviews"], serde_json::json!(["tab-*"]));
|
||||
assert_eq!(tabs["local"], false);
|
||||
|
||||
let local = read(include_str!("../capabilities/workspace-local.json"));
|
||||
assert!(local.get("remote").is_none());
|
||||
// My Space (profile customization) is removed: its commands do not exist, so no capability
|
||||
// file grants them and neither the command manifest nor the handler names them.
|
||||
let (build, desktop) = (include_str!("../build.rs"), include_str!("../src/desktop.rs"));
|
||||
for gone in [
|
||||
"profile_owner_get", "profile_create", "profile_api", "profile_media_pick", "profile_media_still", "profile_media_upload", "profile_still_to_x", "profile_view_get", "profile_view_set",
|
||||
"bt_profiles_resolve", "bt_media_get", "bt_profile_link_seen",
|
||||
] {
|
||||
let permission = format!("allow-{}", gone.replace('_', "-"));
|
||||
for (name, capability) in [("x-capture", &x), ("drafts-popover", &popover), ("tab-chrome", &tabs), ("workspace-local", &local)] {
|
||||
assert!(!capability["permissions"].as_array().unwrap().iter().any(|p| p == permission.as_str()), "{name} grants {permission}");
|
||||
}
|
||||
assert!(!build.contains(&format!("\"{gone}\"")), "build.rs still lists {gone}");
|
||||
assert!(!desktop.contains(gone), "desktop.rs still names {gone}");
|
||||
}
|
||||
assert_eq!(local["webviews"], serde_json::json!(["shell"]));
|
||||
|
||||
// Update commands are shell-only; no capability exposes the updater/process plugin JS APIs.
|
||||
let perms = |v: &serde_json::Value| v["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_owned()).collect::<Vec<_>>();
|
||||
// Account profiles (separate X logins) are managed only by the shell.
|
||||
// Drafts are inserted and alert keywords edited only by the shell; X tabs only ask/report.
|
||||
for name in [
|
||||
"allow-update-status", "allow-apply-update", "allow-app-ready", "allow-profiles-get", "allow-profiles-set",
|
||||
"allow-tab-insert-text", "allow-alerts-get", "allow-alerts-set", "allow-draft-list", "allow-draft-revisions",
|
||||
// Google Fonts downloads (network), account/pane themes and the builder preview: shell only.
|
||||
"allow-gfonts-installed", "allow-gfonts-install", "allow-gfonts-remove", "allow-gfonts-faces", "allow-gfonts-preview",
|
||||
"allow-theme-scopes-get", "allow-theme-scopes-set", "allow-theme-preview",
|
||||
// Installing themes/plugins (links, dropped files, the signed store; owner-approved 2026-09-28):
|
||||
// only the shell, after its confirmation screen. X tabs and web pages cannot install anything.
|
||||
"allow-install-pending", "allow-install-stage-link", "allow-install-stage-file", "allow-install-cancel",
|
||||
"allow-install-commit", "allow-install-records", "allow-install-forget", "allow-plugin-remove",
|
||||
"allow-store-catalog", "allow-store-stage",
|
||||
] {
|
||||
assert!(perms(&local).iter().any(|p| p == name), "shell lacks {name}");
|
||||
assert!(!perms(&x).iter().chain(perms(&tabs).iter()).any(|p| p == name), "tab has {name}");
|
||||
}
|
||||
for p in perms(&local).iter().chain(perms(&x).iter()).chain(perms(&tabs).iter()) {
|
||||
assert!(
|
||||
!p.starts_with("updater:") && !p.starts_with("process:") && !p.starts_with("notification:") && !p.starts_with("deep-link:"),
|
||||
"plugin API exposed: {p}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_accepts_only_known_presets_and_hex_accents() {
|
||||
let script = |theme: XTheme| theme.script(&X_THEMES, &SITE);
|
||||
let valid = XTheme { preset: "night-ink".into(), accent: Some("#1d9bf0".into()), ..x_default() };
|
||||
let text = script(valid).unwrap();
|
||||
assert!(text.contains("location.origin!=='https://x.com'"));
|
||||
assert!(text.contains(r#""preset":"night-ink""#));
|
||||
assert!(x_default().is_default(&X_THEMES));
|
||||
for preset in ["x", "classic", "night-ink", "terminal", "paper"] {
|
||||
assert!(script(XTheme { preset: preset.into(), ..x_default() }).is_ok());
|
||||
}
|
||||
for hostile in ["evil", "x;display:none", "</script>", ""] {
|
||||
assert!(script(XTheme { preset: hostile.into(), ..x_default() }).is_err(), "accepted {hostile}");
|
||||
}
|
||||
for hostile in ["red", "#fff", "url(https://evil.test)", "#000000;display:none", "</style><script>", "#00000é"] {
|
||||
assert!(script(XTheme { accent: Some(hostile.into()), ..x_default() }).is_err(), "accepted {hostile}");
|
||||
}
|
||||
assert!(serde_json::from_str::<XTheme>(r#"{"preset":"x","css":"body{}"}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_keeps_macos_targets_and_signed_updater() {
|
||||
let conf: serde_json::Value = serde_json::from_str(include_str!("../tauri.conf.json")).unwrap();
|
||||
let targets = conf["bundle"]["targets"].as_array().unwrap();
|
||||
for target in ["nsis", "app", "dmg"] {
|
||||
assert!(targets.iter().any(|t| t == target), "missing bundle target {target}");
|
||||
}
|
||||
assert!(!conf["plugins"]["updater"]["pubkey"].as_str().unwrap_or("").is_empty());
|
||||
for endpoint in conf["plugins"]["updater"]["endpoints"].as_array().unwrap() {
|
||||
assert!(endpoint.as_str().unwrap().starts_with("https://"), "updater endpoint must be https");
|
||||
}
|
||||
// One URL scheme, only for install links; the installer/Info.plist register exactly this.
|
||||
assert_eq!(conf["plugins"]["deep-link"]["desktop"]["schemes"], serde_json::json!(["waybetter"]));
|
||||
assert!(conf["plugins"]["deep-link"].get("mobile").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backup_sync_workspace_palette_and_dev_commands_are_shell_only() {
|
||||
let read = |json: &str| serde_json::from_str::<serde_json::Value>(json).unwrap();
|
||||
let perms = |v: &serde_json::Value| v["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_owned()).collect::<Vec<_>>();
|
||||
let local = perms(&read(include_str!("../capabilities/workspace-local.json")));
|
||||
let x = perms(&read(include_str!("../capabilities/x-capture.json")));
|
||||
let tabs = perms(&read(include_str!("../capabilities/tab-chrome.json")));
|
||||
let popover = perms(&read(include_str!("../capabilities/drafts-popover.json")));
|
||||
// Settings backup/sync (files and the sync key stay native), workspaces, running a plugin's
|
||||
// palette command in a tab, and developer mode (file watcher, hot reload).
|
||||
for name in [
|
||||
"allow-tab-run-command", "allow-workspaces-get", "allow-workspaces-set",
|
||||
"allow-backup-collect", "allow-backup-export-file", "allow-backup-open-file", "allow-backup-apply",
|
||||
"allow-sync-status", "allow-sync-choose-folder", "allow-sync-unlock", "allow-sync-lock", "allow-sync-disable",
|
||||
"allow-sync-configure", "allow-sync-push", "allow-sync-check", "allow-sync-open", "allow-sync-mark-pulled",
|
||||
"allow-dev-get", "allow-dev-set", "allow-dev-pick-theme", "allow-dev-clear-theme", "allow-dev-reload-plugin", "allow-dev-report-error",
|
||||
] {
|
||||
assert!(local.iter().any(|p| p == name), "shell lacks {name}");
|
||||
assert!(!x.iter().chain(tabs.iter()).chain(popover.iter()).any(|p| p == name), "a tab or the popover has {name}");
|
||||
}
|
||||
// X tabs report their plugins' commands only through tab_report (owner-approved 2026-09-28);
|
||||
// the tab capabilities gained nothing for it.
|
||||
assert_eq!(tabs, ["allow-tab-report", "allow-tab-shortcut", "allow-tab-link-drag"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_commands_are_shell_only() {
|
||||
let read = |json: &str| serde_json::from_str::<serde_json::Value>(json).unwrap();
|
||||
let perms = |v: &serde_json::Value| v["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_owned()).collect::<Vec<_>>();
|
||||
let local = perms(&read(include_str!("../capabilities/workspace-local.json")));
|
||||
let others: Vec<String> = [
|
||||
include_str!("../capabilities/x-capture.json"),
|
||||
include_str!("../capabilities/tab-chrome.json"),
|
||||
include_str!("../capabilities/drafts-popover.json"),
|
||||
]
|
||||
.iter()
|
||||
.flat_map(|json| perms(&read(json)))
|
||||
.collect();
|
||||
// The shell picks the language and hands native code and X tabs their strings; tabs and the
|
||||
// popover only ever receive them (X tabs as data through the runtime), never set them.
|
||||
for command in ["allow-i18n-get", "allow-i18n-set"] {
|
||||
assert!(local.iter().any(|p| p == command), "shell lacks {command}");
|
||||
assert!(!others.iter().any(|p| p == command), "a tab or the popover has {command}");
|
||||
}
|
||||
// The commands are the framework's (waybetter-host `app`); each checks shell_only first.
|
||||
for command in ["i18n_get", "i18n_set"] {
|
||||
waybetter_host::testkit::assert_guarded(include_str!("../../../../waybetter/crates/waybetter-host/src/app.rs"), command, "shell_only(&webview)?");
|
||||
}
|
||||
// X tabs get the strings only as a guarded data value, and only the runtime's own prefixes.
|
||||
let source = include_str!("../src/desktop_i18n.rs");
|
||||
assert!(source.contains(r#"&["x.", "kit.", "plugin."]"#), "X tabs receive only x./kit./plugin. strings");
|
||||
assert!(include_str!("../src/desktop.rs").contains("page_string_prefixes: crate::desktop_i18n::PAGE_PREFIXES"), "X's config hands tabs exactly those");
|
||||
let strings = waybetter_host::i18n::NativeStrings {
|
||||
locale: "tr".into(),
|
||||
preference: "site".into(),
|
||||
messages: [("x.fab.title", "wayBetter"), ("kit.ok", "Tamam"), ("native.notify.alert.title", "Uyarı")]
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.to_owned(), value.to_owned()))
|
||||
.collect(),
|
||||
};
|
||||
let script = waybetter_host::i18n::page_script(&better_twttr::site::SITE, &strings.page_payload(&["x.", "kit.", "plugin."]));
|
||||
assert!(script.starts_with("if(location.origin==='https://x.com')("), "page strings must be x.com only: {script}");
|
||||
assert!(script.contains("x.fab.title") && script.contains("kit.ok") && !script.contains("native.notify"), "{script}");
|
||||
// "Follow X": a tab may report X's <html lang>, only as a valid tag and only while on x.com.
|
||||
let desktop = include_str!("../src/desktop.rs");
|
||||
assert!(
|
||||
desktop.contains("report.lang.filter(|lang| waybetter_host::i18n::valid_tag(lang) && webview.url().is_ok_and(|url| policy::x_origin(&url)))"),
|
||||
"tab_report lang must be a validated tag from a webview on x.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_strings_are_plain_text() {
|
||||
use waybetter_host::i18n::NativeStrings;
|
||||
let set = |value: &str| NativeStrings {
|
||||
locale: "ar".into(),
|
||||
preference: "site".into(),
|
||||
messages: [("x.fab.title".to_owned(), value.to_owned())].into_iter().collect(),
|
||||
};
|
||||
assert!(set("wayBetter لـ X").validated().is_ok());
|
||||
for bad in ["<img src=x onerror=alert(1)>", "<script>", "javascript:alert(1)", "a\u{202e}b"] {
|
||||
assert!(set(bad).validated().is_err(), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_raise_is_shell_only() {
|
||||
let read = |json: &str| serde_json::from_str::<serde_json::Value>(json).unwrap();
|
||||
let perms = |v: &serde_json::Value| v["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_owned()).collect::<Vec<_>>();
|
||||
let local = perms(&read(include_str!("../capabilities/workspace-local.json")));
|
||||
let x = perms(&read(include_str!("../capabilities/x-capture.json")));
|
||||
let tabs = perms(&read(include_str!("../capabilities/tab-chrome.json")));
|
||||
let popover = perms(&read(include_str!("../capabilities/drafts-popover.json")));
|
||||
// Re-orders the shell webview over the tabs for its own overlays: only the shell may.
|
||||
assert!(local.iter().any(|p| p == "allow-shell-raise"), "shell lacks allow-shell-raise");
|
||||
assert!(!x.iter().chain(tabs.iter()).chain(popover.iter()).any(|p| p == "allow-shell-raise"), "a tab or the popover can raise the shell");
|
||||
// The command is the framework's (waybetter-host `app`).
|
||||
waybetter_host::testkit::assert_guarded(include_str!("../../../../waybetter/crates/waybetter-host/src/app.rs"), "shell_raise", "shell_only(&webview)?");
|
||||
waybetter_host::testkit::Capabilities::from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/capabilities")).assert_shell_only("shell_raise");
|
||||
}
|
||||
|
||||
/// The wayBetter host's own commands (tab layout and actions, shell raise, external links, window
|
||||
/// menu, profiles, language; tab shortcuts and link drags) keep their callers in X's capability
|
||||
/// files, and every command wrapper checks its caller first. Every capability file is read, so a
|
||||
/// new one cannot quietly widen them.
|
||||
#[test]
|
||||
fn host_commands_keep_their_callers() {
|
||||
use waybetter_host::testkit::{assert_guarded, assert_host_guards, Capabilities};
|
||||
let caps = Capabilities::from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/capabilities"));
|
||||
caps.assert_host_commands();
|
||||
assert_host_guards();
|
||||
// Only Rust drives the plugins; no capability exposes their JS APIs.
|
||||
caps.assert_no_plugin_apis(&["updater", "process", "notification", "deep-link", "single-instance", "window-state", "opener", "dialog"]);
|
||||
// X's own: vault, settings, themes, installs, backups, sync, dev mode, updates are the shell's.
|
||||
for command in [
|
||||
"draft_list", "draft_save", "draft_revisions", "draft_status", "observation_list", "data_export",
|
||||
"gfonts_installed", "gfonts_install", "gfonts_remove", "gfonts_faces", "gfonts_preview",
|
||||
"tab_run_command", "workspaces_get", "workspaces_set", "backup_collect", "backup_export_file", "backup_open_file", "backup_apply",
|
||||
"sync_status", "sync_choose_folder", "sync_unlock", "sync_lock", "sync_disable", "sync_configure", "sync_push", "sync_check", "sync_open", "sync_mark_pulled",
|
||||
"dev_get", "dev_set", "dev_pick_theme", "dev_clear_theme", "dev_reload_plugin", "dev_report_error",
|
||||
"plugins_list", "plugin_set_enabled", "plugins_open_folder", "plugin_remove",
|
||||
"install_pending", "install_stage_link", "install_stage_file", "install_stage_opened", "install_cancel", "install_commit", "install_records", "install_forget", "store_catalog", "store_stage",
|
||||
"custom_css_get", "custom_css_set", "features_get", "features_set", "mode_get", "mode_set", "settings_get", "settings_set", "column_configure",
|
||||
"update_status", "update_check", "apply_update", "app_ready", "tab_insert_text", "alerts_get", "alerts_set",
|
||||
"media_get", "media_set", "gallery_add_folder", "gallery_list", "gallery_thumb", "clip_list", "clip_thumb", "clip_remove", "tab_attach_image",
|
||||
] {
|
||||
caps.assert_shell_only(command);
|
||||
}
|
||||
// Tabs, about themselves only; the X-only ones are granted only on https://x.com.
|
||||
for command in ["tab_report", "capture_draft", "open_panel", "drafts_picker_open", "drafts_popover_dismiss", "drafts_hint", "keyword_alert", "link_preview"] {
|
||||
caps.assert_tab_only(command);
|
||||
}
|
||||
let desktop = include_str!("../src/desktop.rs");
|
||||
assert_guarded(desktop, "tab_insert_text", "shell_only(&webview)?");
|
||||
// The vault commands are the host's: shell-only in the capability files and each checking its caller
|
||||
// first; X's own handler no longer has them, and X turns the vault on with its rules.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::VAULT);
|
||||
waybetter_host::testkit::assert_vault_guards();
|
||||
assert!(desktop.contains(".with_vault("), "the app enables the host's vault");
|
||||
for command in waybetter_host::commands::VAULT {
|
||||
assert!(!desktop.contains(&format!("fn {command}(")), "{command} is served by the host only");
|
||||
}
|
||||
// The updater commands are the shell's too, each checking its caller first; `update_check` runs the
|
||||
// timer's own check once (no other endpoint), and tabs have no updater command at all.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::UPDATE);
|
||||
waybetter_host::testkit::assert_update_guards();
|
||||
assert!(desktop.contains(".with_updates("), "the app enables the updater");
|
||||
// Plugins, custom CSS, the tweak flags and the browser settings are served by the host: they
|
||||
// are in its shell-only list (checked against the capability files and for their caller
|
||||
// check above), and X's own handler no longer has them.
|
||||
let host = include_str!("../../../../waybetter/crates/waybetter-host/src/app.rs");
|
||||
for command in ["plugins_list", "plugin_set_enabled", "plugins_open_folder", "custom_css_get", "custom_css_set", "features_get", "features_set", "mode_get", "mode_set", "settings_get", "settings_set"] {
|
||||
assert!(waybetter_host::commands::SHELL.contains(&command), "{command} is a host shell-only command");
|
||||
assert_guarded(host, command, "shell_only(&webview)?");
|
||||
assert!(!desktop.contains(&format!("fn {command}(")), "{command} is served by the host only");
|
||||
}
|
||||
assert_guarded(desktop, "tab_report", "tab_only(&webview)?");
|
||||
// Opening the panel from a page is the host's site command: only X tabs on x.com have it.
|
||||
caps.assert_site_only("open_panel", &better_twttr::site::SITE);
|
||||
assert!(!desktop.contains("fn open_panel("), "open_panel is served by the host only");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_store_media_and_font_commands_are_shell_only() {
|
||||
// The store's commands are the framework's (waybetter-host `store_commands`, served because
|
||||
// this app sets `AppConfig::store`): shell-only in the capability files, and each wrapper
|
||||
// checks shell_only before anything else.
|
||||
use waybetter_host::testkit::assert_guarded;
|
||||
let caps = waybetter_host::testkit::Capabilities::from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/capabilities"));
|
||||
caps.assert_shell_only_group(waybetter_host::commands::STORE);
|
||||
waybetter_host::testkit::assert_store_guards();
|
||||
// The media panel's commands are the framework's too (AppConfig::with_media): shell-only, each
|
||||
// checking its caller first; only attaching to X's composer is the app's own.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::MEDIA);
|
||||
waybetter_host::testkit::assert_media_guards();
|
||||
// Workspaces and encrypted sync too (AppConfig::with_sync; workspaces are served for every app).
|
||||
caps.assert_shell_only_group(waybetter_host::commands::WORKSPACES);
|
||||
caps.assert_shell_only_group(waybetter_host::commands::SYNC);
|
||||
waybetter_host::testkit::assert_sync_guards();
|
||||
// The settings backup's commands are the framework's too (what it carries is the app's hooks).
|
||||
caps.assert_shell_only_group(waybetter_host::commands::BACKUP);
|
||||
waybetter_host::testkit::assert_backup_guards();
|
||||
// Developer mode (AppConfig::with_dev): the framework's commands; the theme file is the app's hook.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::DEV);
|
||||
waybetter_host::testkit::assert_dev_guards();
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_dev()"), "the app enables developer mode");
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_sync()"), "the app enables sync");
|
||||
// Site themes too (AppConfig::with_themes): the framework's commands; X only names its built-in themes.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::THEME);
|
||||
waybetter_host::testkit::assert_theme_guards();
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_themes(&crate::theme::THEMES)"), "the app configures its themes");
|
||||
// The keyword alert settings (AppConfig::with_alerts): the framework's commands; the hit and its notification are X's.
|
||||
caps.assert_shell_only_group(waybetter_host::commands::ALERTS);
|
||||
waybetter_host::testkit::assert_alerts_guards();
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_alerts(&alerts::ALERTS)"), "the app configures its alerts");
|
||||
// Google Fonts too (AppConfig::with_fonts).
|
||||
caps.assert_shell_only_group(waybetter_host::commands::GFONTS);
|
||||
waybetter_host::testkit::assert_fonts_guards();
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_fonts()"), "the app enables Google Fonts");
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_media()"), "the app enables the media panel");
|
||||
assert_guarded(include_str!("../src/desktop_media.rs"), "tab_attach_image", "shell_only(&webview)?");
|
||||
let source = include_str!("../../../../waybetter/crates/waybetter-host/src/store_commands.rs");
|
||||
// Links only ever propose an install: the deep-link handler queues and emits, nothing more.
|
||||
let received = &source[source.find("fn received(").unwrap()..];
|
||||
let received = &received[..received.find("\n}").unwrap()];
|
||||
assert!(!received.contains("commit") && !received.contains("stage"), "a link must not stage or install by itself");
|
||||
// The app's store settings: X's id and the framework's hosts; the compiled-in key; and it asks
|
||||
// the framework to serve the store.
|
||||
let settings = include_str!("../src/distribution.rs");
|
||||
assert!(settings.contains("app_id: \"x\"") && settings.contains("allowed_hosts: DEFAULT_HOSTS") && settings.contains("include_str!(\"../keys/store.pub\")"));
|
||||
assert!(include_str!("../src/desktop.rs").contains(".with_store(&crate::distribution::STORE)"), "the app configures the store");
|
||||
}
|
||||
|
||||
/// The popover's webview and session are the host's; X's commands reached by the popover each check
|
||||
/// their caller first (the popover webview with the open session), and inserting a draft only
|
||||
/// focuses the tab and types (`compose` scripts), never clicks Post.
|
||||
#[test]
|
||||
fn popover_commands_check_their_caller_and_never_post() {
|
||||
use waybetter_host::testkit::assert_guarded;
|
||||
let source = include_str!("../src/desktop_popover.rs");
|
||||
assert_guarded(source, "popover_state", "host::session_of(&webview)");
|
||||
for command in ["popover_drafts", "popover_insert", "popover_open_vault"] {
|
||||
assert_guarded(source, command, "host::current(&webview,");
|
||||
}
|
||||
// popover_close only acts for the current popover, and does nothing otherwise.
|
||||
let close = &source[source.find("fn popover_close(").unwrap()..];
|
||||
let close = &close[..close.find("
|
||||
}").unwrap()];
|
||||
assert!(close.contains("host::current(&webview, seq).is_ok()"));
|
||||
let insert = &source[source.find("fn popover_insert(").unwrap()..];
|
||||
let insert = &insert[..insert.find("
|
||||
}").unwrap()];
|
||||
assert!(insert.contains("compose::insert_script") && insert.contains("compose::append_script"));
|
||||
assert!(!insert.contains("click") && !insert.contains("submit") && !insert.contains("Post"), "popover_insert never posts");
|
||||
assert!(insert.contains("policy::x_origin(&url)"), "only into X tabs");
|
||||
// The host's popover check: only a `popover-<n>` webview on the app's own local origin.
|
||||
let host = include_str!("../../../../waybetter/crates/waybetter-host/src/popover_host.rs");
|
||||
let only = &host[host.find("pub fn popover_only(").unwrap()..];
|
||||
let only = &only[..only.find("
|
||||
}").unwrap()];
|
||||
assert!(only.contains("popover::valid_label(webview.label())") && only.contains("local_navigation_allowed"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue