- one version: package.json's version is the desktop app's too (desktopVersion removed); tools/desktop-version.ts keeps the plugin manifests and the app's copies equal, desktop-release publish refuses another version, release order app first then npm publish; the unpublished desktop 0.1.0 candidate is noted in the docs and CHANGELOG - storebound desktop installs (per user, no admin: macOS DMG -> /Applications or ~/Applications, Windows NSIS /S, Linux AppImage + .desktop with the storebound:// handler), updates an older app, notes a newer one (downgrade only with --force), asks once ([Y/n], --yes, exit 3 without a terminal), opens on the folder; --dry / --json / --download; the install steps are pure plans (cli/lib/desktop-install.ts); the version's own manifest dl/<version>/latest.json - credentials: one store, the CLI's credential profiles in ~/.storebound/credentials; the desktop writes and reads them (profiles.rs, files 0600), moves old keychain accounts into profiles on first start, shows profiles the CLI made; precedence env > named profile > project file > default profile; config `account`, "account" in the project's credentials.json; storebound accounts list | import (dedup by key id / service account e-mail, key files copied 0600, project file keeps keystore and review contact, backup, --dry, never prints values) | use - app folders: one list ~/.storebound/projects.json shared by the desktop (registry.rs), storebound desktop, apps folders | add | remove, ws init and ws without a workspace file - desktop: notice when a folder pins another storebound version; storebound-desktop --version - tests: version drift, desktop decisions and per-OS plans, credential precedence and import, registry (TS + Rust), keychain migration and profile round trip (Rust), boot check with old keychain state and the new shared state
105 lines
5.2 KiB
Rust
105 lines
5.2 KiB
Rust
//! D5 release pipeline: one version everywhere, the updater's key and endpoints, the `storebound://open?path=` deep
|
|
//! link and the self-test the release runs on the exact artifact.
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
use base64::Engine;
|
|
use serde_json::Value;
|
|
use storebound_desktop::policy::deep_link_open_path;
|
|
use storebound_desktop::{selftest, updates};
|
|
use url::Url;
|
|
|
|
fn root() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
}
|
|
|
|
fn conf() -> Value {
|
|
serde_json::from_str(&fs::read_to_string(root().join("tauri.conf.json")).unwrap()).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn one_version_with_the_cli() {
|
|
let pkg: Value = serde_json::from_str(&fs::read_to_string(root().join("../../../package.json")).unwrap()).unwrap();
|
|
// one product, one number: the desktop app has the npm CLI's version (its bundled sidecar is that CLI)
|
|
let want = pkg["version"].as_str().expect("package.json version");
|
|
assert_eq!(env!("CARGO_PKG_VERSION"), want, "Cargo.toml (bun tools/desktop-version.ts --set {want})");
|
|
assert_eq!(conf()["version"], want, "tauri.conf.json");
|
|
let app: Value = serde_json::from_str(&fs::read_to_string(root().join("../package.json")).unwrap()).unwrap();
|
|
assert_eq!(app["version"], want, "apps/desktop/package.json");
|
|
}
|
|
|
|
#[test]
|
|
fn updater_has_a_real_minisign_key_and_only_storebound_dev() {
|
|
let c = conf();
|
|
let key = c["plugins"]["updater"]["pubkey"].as_str().unwrap();
|
|
let text = String::from_utf8(base64::engine::general_purpose::STANDARD.decode(key).expect("base64 pubkey")).unwrap();
|
|
let mut lines = text.lines();
|
|
assert!(lines.next().unwrap().starts_with("untrusted comment: minisign public key"));
|
|
let raw = base64::engine::general_purpose::STANDARD.decode(lines.next().unwrap()).unwrap();
|
|
assert_eq!(raw.len(), 42, "Ed + key id + 32-byte key");
|
|
assert_eq!(&raw[..2], b"Ed");
|
|
assert_eq!(c["plugins"]["updater"]["endpoints"], serde_json::json!([updates::STABLE]));
|
|
// updater bundles are signed by tools/desktop-release.ts on the release Mac, never by a CI runner
|
|
assert_eq!(c["bundle"]["createUpdaterArtifacts"], false);
|
|
for ch in ["stable", "beta", "nightly", ""] {
|
|
assert!(updates::endpoint_allowed(&Url::parse(updates::endpoint(ch)).unwrap()), "{ch}");
|
|
}
|
|
assert_eq!(updates::endpoint("beta"), updates::BETA);
|
|
assert_eq!(updates::endpoint("anything"), updates::STABLE);
|
|
for bad in ["http://storebound.dev/desktop/latest.json", "https://storebound.dev:8443/desktop/latest.json", "https://evil.test/desktop/latest.json", "https://storebound.dev/latest.json"] {
|
|
assert!(!updates::endpoint_allowed(&Url::parse(bad).unwrap()), "{bad}");
|
|
}
|
|
assert!(updates::error_text("signature verification failed").contains("not installed"));
|
|
}
|
|
|
|
#[test]
|
|
fn open_deep_link_takes_one_plain_absolute_path() {
|
|
let p = |s: &str| deep_link_open_path(&Url::parse(s).unwrap());
|
|
let abs = if cfg!(windows) { "C:%5Cgames%5CDemo App" } else { "%2FUsers%2Fyou%2Fgames%2FDemo App" };
|
|
let want = if cfg!(windows) { "C:\\games\\Demo App" } else { "/Users/you/games/Demo App" };
|
|
assert_eq!(p(&format!("storebound://open?path={abs}")), Some(PathBuf::from(want)));
|
|
assert_eq!(p(&format!("storebound://open/?path={abs}")), Some(PathBuf::from(want)));
|
|
for bad in [
|
|
"storebound://open?path=games%2FDemo App".to_string(),
|
|
"storebound://open?path=".to_string(),
|
|
"storebound://open".to_string(),
|
|
format!("storebound://open?path={abs}&path={abs}"),
|
|
format!("storebound://open?path={abs}&run=1"),
|
|
format!("storebound://open?path={abs}%2F..%2F..%2Fetc"),
|
|
format!("storebound://open?path={abs}%0A"),
|
|
format!("storebound://open/x?path={abs}"),
|
|
format!("storebound://open?path={abs}#f"),
|
|
format!("storebound://user@open?path={abs}"),
|
|
format!("storebound://apps?path={abs}"),
|
|
format!("https://open?path={abs}"),
|
|
format!("storebound://open?path={}", "a".repeat(5000)),
|
|
] {
|
|
assert_eq!(p(&bad), None, "{bad}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn self_test_looks_for_the_sidecar_next_to_the_executable() {
|
|
let p = selftest::sidecar_path().unwrap();
|
|
let exe = std::env::current_exe().unwrap();
|
|
assert_eq!(p.parent(), exe.parent());
|
|
assert_eq!(p.file_name().unwrap().to_string_lossy(), format!("storebound{}", std::env::consts::EXE_SUFFIX));
|
|
assert_eq!(selftest::FLAG, "--self-test");
|
|
// the flag is handled in main before Tauri starts, so no window opens
|
|
let main = fs::read_to_string(root().join("src/main.rs")).unwrap();
|
|
let flag = main.find("selftest::FLAG").unwrap();
|
|
assert!(flag < main.find("storebound_desktop::run()").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn deep_links_to_folders_ask_before_adding() {
|
|
let cmds = fs::read_to_string(root().join("src/commands.rs")).unwrap();
|
|
let at = cmds.find("async fn open_folder(").unwrap();
|
|
let body = &cmds[at..];
|
|
let confirm = body.find("confirm_dialog_with(").unwrap();
|
|
let add = body.find("add_folder(").unwrap();
|
|
assert!(confirm < add, "the native dialog comes before add_folder");
|
|
let at = cmds.find("pub async fn update_install(").unwrap();
|
|
let body = &cmds[at..];
|
|
assert!(body.find("confirm_dialog_with(").unwrap() < body.find("download_and_install(").unwrap());
|
|
}
|