refactor: rename aim to upm and extract appimage module
This commit is contained in:
parent
af13e98eb3
commit
863c57e473
117 changed files with 2622 additions and 887 deletions
75
crates/upm/tests/cli_commands.rs
Normal file
75
crates/upm/tests/cli_commands.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use assert_cmd::Command;
|
||||
use predicates::str::contains;
|
||||
|
||||
use clap::Parser;
|
||||
use upm::cli::args::Command as UpmCommand;
|
||||
use upm::{Cli, DispatchError};
|
||||
use upm_core::domain::show::{ShowResultError, SourceSummary};
|
||||
use upm_core::domain::source::SourceKind;
|
||||
|
||||
#[test]
|
||||
fn help_lists_expected_commands() {
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
cmd.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("search"))
|
||||
.stdout(contains("show"))
|
||||
.stdout(contains("remove"))
|
||||
.stdout(contains("list"))
|
||||
.stdout(contains("update"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_parses_show_subcommand() {
|
||||
let cli = Cli::try_parse_from(["upm", "show", "legacy-bat"]).unwrap();
|
||||
|
||||
match cli.command {
|
||||
Some(UpmCommand::Show { value }) => assert_eq!(value.as_deref(), Some("legacy-bat")),
|
||||
other => panic!("expected show command, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_parses_bare_show_subcommand() {
|
||||
let cli = Cli::try_parse_from(["upm", "show"]).unwrap();
|
||||
|
||||
match cli.command {
|
||||
Some(UpmCommand::Show { value }) => assert_eq!(value, None),
|
||||
other => panic!("expected bare show command, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_ambiguity_error_is_readable() {
|
||||
let error = DispatchError::Show(ShowResultError::AmbiguousInstalledMatch {
|
||||
query: "bat".to_owned(),
|
||||
matches: vec![
|
||||
"Bat (bat)".to_owned(),
|
||||
"Bat Preview (legacy-bat)".to_owned(),
|
||||
],
|
||||
});
|
||||
|
||||
let rendered = error.to_string();
|
||||
|
||||
assert!(rendered.contains("multiple installed apps match bat"));
|
||||
assert!(rendered.contains("Bat (bat)"));
|
||||
assert!(rendered.contains("Bat Preview (legacy-bat)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_no_installable_artifact_error_is_readable() {
|
||||
let error = DispatchError::Show(ShowResultError::NoInstallableArtifact {
|
||||
source: SourceSummary {
|
||||
kind: SourceKind::SourceForge,
|
||||
locator: "https://sourceforge.net/projects/team-app/".to_owned(),
|
||||
canonical_locator: Some("team-app".to_owned()),
|
||||
},
|
||||
});
|
||||
|
||||
let rendered = error.to_string();
|
||||
|
||||
assert!(rendered.contains("no installable artifact found"));
|
||||
assert!(rendered.contains("sourceforge"));
|
||||
assert!(rendered.contains("https://sourceforge.net/projects/team-app/"));
|
||||
}
|
||||
7
crates/upm/tests/cli_smoke.rs
Normal file
7
crates/upm/tests/cli_smoke.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use assert_cmd::Command;
|
||||
|
||||
#[test]
|
||||
fn cli_shows_help() {
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
cmd.arg("--help").assert().success();
|
||||
}
|
||||
167
crates/upm/tests/config_loading.rs
Normal file
167
crates/upm/tests/config_loading.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use tempfile::tempdir;
|
||||
use upm::config::{
|
||||
CliConfig, ConfigError, SearchConfig, ThemeConfig, default_path, load_from_path,
|
||||
};
|
||||
use upm::default_registry_path;
|
||||
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct EnvGuard {
|
||||
key: &'static str,
|
||||
original: Option<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
|
||||
let original = std::env::var_os(key);
|
||||
unsafe {
|
||||
std::env::set_var(key, value);
|
||||
}
|
||||
Self { key, original }
|
||||
}
|
||||
|
||||
fn remove(key: &'static str) -> Self {
|
||||
let original = std::env::var_os(key);
|
||||
unsafe {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard {
|
||||
fn drop(&mut self) {
|
||||
match &self.original {
|
||||
Some(value) => unsafe {
|
||||
std::env::set_var(self.key, value);
|
||||
},
|
||||
None => unsafe {
|
||||
std::env::remove_var(self.key);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_config_file_returns_defaults() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
|
||||
let config = load_from_path(&path).unwrap();
|
||||
|
||||
assert_eq!(config, CliConfig::default());
|
||||
assert_eq!(config.search, SearchConfig::default());
|
||||
assert!(!config.allow_http);
|
||||
assert!(config.search.bottom_to_top);
|
||||
assert!(!config.search.skip_confirmation);
|
||||
assert_eq!(config.theme.accent, "#b388ff");
|
||||
assert_eq!(config.theme.accent_secondary, "#d5c2ff");
|
||||
assert_eq!(config.theme.dim, "#7f7396");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_section_overrides_defaults() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&path,
|
||||
"allow_http = true\n\n[search]\nbottom_to_top = false\nskip_confirmation = true\n\n[theme]\naccent = \"#9f6bff\"\naccent_secondary = \"#efe7ff\"\ndim = \"#6b6480\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_from_path(&path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
CliConfig {
|
||||
allow_http: true,
|
||||
search: SearchConfig {
|
||||
bottom_to_top: false,
|
||||
skip_confirmation: true,
|
||||
},
|
||||
theme: ThemeConfig {
|
||||
accent: "#9f6bff".to_owned(),
|
||||
accent_secondary: "#efe7ff".to_owned(),
|
||||
dim: "#6b6480".to_owned(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_toml_returns_path_aware_error() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(&path, "[search\nskip_confirmation = true\n").unwrap();
|
||||
|
||||
let error = load_from_path(&path).unwrap_err();
|
||||
|
||||
match error {
|
||||
ConfigError::Parse {
|
||||
path: error_path, ..
|
||||
} => {
|
||||
assert_eq!(error_path, path);
|
||||
}
|
||||
other => panic!("expected parse error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_config_path_uses_upm_directory() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let dir = tempdir().unwrap();
|
||||
|
||||
let _config_path = EnvGuard::remove("UPM_CONFIG_PATH");
|
||||
let _xdg_config_home = EnvGuard::remove("XDG_CONFIG_HOME");
|
||||
let _home = EnvGuard::set("HOME", dir.path());
|
||||
|
||||
let path = default_path();
|
||||
|
||||
assert_eq!(path, dir.path().join(".config/upm/config.toml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_config_path_ignores_legacy_aim_override() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let dir = tempdir().unwrap();
|
||||
let legacy_path = dir.path().join("aim-config.toml");
|
||||
|
||||
let _legacy_config_path = EnvGuard::set("AIM_CONFIG_PATH", &legacy_path);
|
||||
let _config_path = EnvGuard::remove("UPM_CONFIG_PATH");
|
||||
let _xdg_config_home = EnvGuard::remove("XDG_CONFIG_HOME");
|
||||
let _home = EnvGuard::set("HOME", dir.path());
|
||||
|
||||
let path = default_path();
|
||||
|
||||
assert_eq!(path, dir.path().join(".config/upm/config.toml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_registry_path_uses_upm_directory() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let dir = tempdir().unwrap();
|
||||
|
||||
let _registry_path = EnvGuard::remove("UPM_REGISTRY_PATH");
|
||||
let _home = EnvGuard::set("HOME", dir.path());
|
||||
|
||||
let path = default_registry_path();
|
||||
|
||||
assert_eq!(path, dir.path().join(".local/share/upm/registry.toml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_registry_path_ignores_legacy_aim_override() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let dir = tempdir().unwrap();
|
||||
let legacy_path = dir.path().join("aim-registry.toml");
|
||||
|
||||
let _legacy_registry_path = EnvGuard::set("AIM_REGISTRY_PATH", &legacy_path);
|
||||
let _registry_path = EnvGuard::remove("UPM_REGISTRY_PATH");
|
||||
let _home = EnvGuard::set("HOME", dir.path());
|
||||
|
||||
let path = default_registry_path();
|
||||
|
||||
assert_eq!(path, dir.path().join(".local/share/upm/registry.toml"));
|
||||
}
|
||||
755
crates/upm/tests/end_to_end_cli.rs
Normal file
755
crates/upm/tests/end_to_end_cli.rs
Normal file
|
|
@ -0,0 +1,755 @@
|
|||
use assert_cmd::Command;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use predicates::str::contains;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use upm_core::registry::model::Registry;
|
||||
use upm_core::registry::store::RegistryStore;
|
||||
|
||||
const FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
|
||||
|
||||
#[test]
|
||||
fn list_command_runs_without_registry_entries() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("list")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("No installed apps yet"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_command_reads_registered_apps_from_registry_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
std::fs::write(
|
||||
®istry_path,
|
||||
"version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("list")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Name"))
|
||||
.stdout(contains("Version"))
|
||||
.stdout(contains("Source"))
|
||||
.stdout(contains("Bat"))
|
||||
.stdout(contains("Bat (bat)").not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_command_removes_registered_app_from_registry_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
std::fs::write(
|
||||
®istry_path,
|
||||
"version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["remove", "bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Removed Bat"))
|
||||
.stdout(contains("Removal Summary").not())
|
||||
.stdout(contains("Removed app:").not());
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(!contents.contains("stable_id = \"bat\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_command_uninstalls_managed_files() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let install_home = dir.path().join("install-home");
|
||||
let payload_path = install_home.join(".local/lib/upm/appimages/sharkdp-bat.AppImage");
|
||||
let desktop_path = install_home.join(".local/share/applications/upm-sharkdp-bat.desktop");
|
||||
let icon_path = install_home.join(".local/share/icons/hicolor/256x256/apps/sharkdp-bat.png");
|
||||
|
||||
let mut add_cmd = Command::cargo_bin("upm").unwrap();
|
||||
add_cmd
|
||||
.arg("sharkdp/bat")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(payload_path.exists());
|
||||
assert!(desktop_path.exists());
|
||||
assert!(icon_path.exists());
|
||||
|
||||
let mut remove_cmd = Command::cargo_bin("upm").unwrap();
|
||||
remove_cmd
|
||||
.args(["remove", "sharkdp-bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("\nRemoved bat"))
|
||||
.stdout(contains("Removed bat"))
|
||||
.stdout(contains("Removal Summary").not())
|
||||
.stdout(contains("Removed app:").not())
|
||||
.stdout(contains("Removed files"))
|
||||
.stdout(contains("sharkdp-bat.AppImage"))
|
||||
.stdout(contains("upm-sharkdp-bat.desktop"))
|
||||
.stdout(contains("sharkdp-bat.png"));
|
||||
|
||||
assert!(!payload_path.exists());
|
||||
assert!(!desktop_path.exists());
|
||||
assert!(!icon_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_command_registers_unambiguous_app_in_registry_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("sharkdp/bat")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("\nInstalled bat (user)"))
|
||||
.stdout(contains("Installed bat (user)"))
|
||||
.stdout(contains("Installation Summary").not())
|
||||
.stdout(contains("Source: github sharkdp/bat"))
|
||||
.stdout(contains("Artifact:"))
|
||||
.stdout(contains("Selected artifact").not())
|
||||
.stdout(contains("metadata-guided").not())
|
||||
.stdout(contains("Installed files"))
|
||||
.stdout(contains("sharkdp-bat.AppImage"))
|
||||
.stdout(contains("Completed steps").not());
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains("stable_id = \"sharkdp-bat\""));
|
||||
assert!(contents.contains("source_input = \"sharkdp/bat\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_release_query_renders_tracking_prompt_without_writing_registry() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Choose update tracking"))
|
||||
.stdout(contains("v0.0.11"))
|
||||
.stdout(contains("v0.0.12"));
|
||||
|
||||
assert!(!registry_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_release_query_can_track_latest_and_register_app() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.env("UPM_TRACKING_PREFERENCE", "latest")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("\nInstalled t3code (user)"))
|
||||
.stdout(contains("Installed t3code (user)"))
|
||||
.stdout(contains("Installation Summary").not())
|
||||
.stdout(contains("Source: github pingdotgg/t3code"))
|
||||
.stdout(contains("Artifact: T3-Code-0.0.12-x86_64.AppImage"))
|
||||
.stdout(contains("Selected artifact").not())
|
||||
.stdout(contains("metadata-guided").not())
|
||||
.stdout(contains("Installed files"))
|
||||
.stdout(contains("pingdotgg-t3code.AppImage"))
|
||||
.stdout(contains("Completed steps").not());
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains("stable_id = \"pingdotgg-t3code\""));
|
||||
assert!(contents.contains("locator = \"pingdotgg/t3code\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_release_query_ignores_legacy_tracking_preference_env() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.env("AIM_TRACKING_PREFERENCE", "latest")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Choose update tracking"))
|
||||
.stdout(contains("v0.0.11"))
|
||||
.stdout(contains("v0.0.12"))
|
||||
.stdout(contains("Installed t3code").not());
|
||||
|
||||
assert!(!registry_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_installs_and_renders_resolved_mode() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("sharkdp/bat")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("\nInstalled bat (user)"))
|
||||
.stdout(contains("Installed bat (user)"))
|
||||
.stdout(contains("Artifact:"))
|
||||
.stdout(contains("Installed files"))
|
||||
.stdout(contains("Completed steps").not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positional_query_falls_back_to_search_for_plain_name_queries() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("firefox")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains(
|
||||
"[appimagehub] Firefox by Mozilla - Official AppImage Edition",
|
||||
))
|
||||
.stdout(contains("Install query: appimagehub/2338455"))
|
||||
.stdout(contains("Installed firefox").not())
|
||||
.stdout(contains("unsupported source query").not());
|
||||
|
||||
assert!(!registry_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positional_query_falls_back_to_empty_search_when_direct_item_has_no_appimage() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("appimagehub/2337998")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("No remote matches"))
|
||||
.stdout(contains("No installed matches"))
|
||||
.stdout(contains("unsupported source query").not())
|
||||
.stdout(contains("no installable artifact").not());
|
||||
|
||||
assert!(!registry_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_installs_appimagehub_source_with_truthful_origin() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("appimagehub/2338455")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains(
|
||||
"Installed Firefox by Mozilla - Official AppImage Edition (user)",
|
||||
))
|
||||
.stdout(contains(
|
||||
"Source: appimagehub https://www.appimagehub.com/p/2338455",
|
||||
))
|
||||
.stdout(contains(
|
||||
"Artifact: https://files06.pling.com/api/files/download/firefox-x86-64.AppImage",
|
||||
));
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains("display_name = \"Firefox by Mozilla - Official AppImage Edition\""));
|
||||
assert!(contents.contains("kind = \"AppImageHub\""));
|
||||
assert!(contents.contains("canonical_locator = \"2338455\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_installs_gitlab_source_with_truthful_origin() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("https://gitlab.com/example/team-app")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed team-app (user)"))
|
||||
.stdout(contains("Source: gitlab https://gitlab.com/example/team-app"))
|
||||
.stdout(contains(
|
||||
"Artifact: https://gitlab.com/example/team-app/-/releases/permalink/latest/downloads/team-app.AppImage",
|
||||
));
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains("source_input = \"https://gitlab.com/example/team-app\""));
|
||||
assert!(contents.contains("kind = \"GitLab\""));
|
||||
assert!(contents.contains("locator = \"https://gitlab.com/example/team-app\""));
|
||||
assert!(contents.contains("canonical_locator = \"example/team-app\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_preserves_direct_url_origin_for_provider_like_downloads() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let query = "https://sourceforge.net/projects/team-app/files/team-app-1.0.0.AppImage/download";
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg(query)
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed "))
|
||||
.stdout(contains(format!("Source: direct-url {query}")))
|
||||
.stdout(contains(format!("Artifact: {query}")));
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains(&format!("source_input = \"{query}\"")));
|
||||
assert!(contents.contains("kind = \"DirectUrl\""));
|
||||
assert!(contents.contains(&format!("locator = \"{query}\"")));
|
||||
assert!(!contents.contains("kind = \"SourceForge\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let query = "https://sourceforge.net/projects/team-app/files/latest/download";
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg(query)
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed team-app (user)"))
|
||||
.stdout(contains(format!("Source: sourceforge {query}")))
|
||||
.stdout(contains(format!("Artifact: {query}")));
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains(&format!("source_input = \"{query}\"")));
|
||||
assert!(contents.contains("kind = \"SourceForge\""));
|
||||
assert!(contents.contains(&format!("locator = \"{query}\"")));
|
||||
assert!(contents.contains("canonical_locator = \"team-app\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_rejects_insecure_http_direct_urls_by_default() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("http://example.com/team-app.AppImage")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("insecure HTTP sources are disabled"));
|
||||
|
||||
assert!(!registry_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_allows_insecure_http_direct_urls_when_config_enables_it() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, "allow_http = true\n").unwrap();
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("http://example.com/team-app.AppImage")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env("UPM_CONFIG_PATH", &config_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed"))
|
||||
.stdout(contains(
|
||||
"Source: direct-url http://example.com/team-app.AppImage",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_rejects_insecure_appimagehub_download_urls_even_when_http_is_allowed() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, "allow_http = true\n").unwrap();
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("appimagehub/2338455")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env("UPM_CONFIG_PATH", &config_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.env("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP", "1")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("insecure appimagehub download url"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_rejects_appimagehub_install_when_md5_does_not_match() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("appimagehub/2338455")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.env("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5", "1")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("weak provider checksum did not match"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_installs_sourceforge_release_folder_with_truthful_origin() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let query = "https://sourceforge.net/projects/team-app/files/releases/beta/download";
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg(query)
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed team-app (user)"))
|
||||
.stdout(contains(format!("Source: sourceforge {query}")))
|
||||
.stdout(contains(format!("Artifact: {query}")));
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains(&format!("source_input = \"{query}\"")));
|
||||
assert!(contents.contains("kind = \"SourceForge\""));
|
||||
assert!(contents.contains(&format!("locator = \"{query}\"")));
|
||||
assert!(contents.contains("canonical_locator = \"team-app\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_file_like_sourceforge_release_download_stores_releases_root_and_preserves_artifact() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let query =
|
||||
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download";
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg(query)
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed team-app (user)"))
|
||||
.stdout(contains(
|
||||
"Source: sourceforge https://sourceforge.net/projects/team-app/files/releases",
|
||||
))
|
||||
.stdout(contains(format!("Artifact: {query}")));
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains(&format!("source_input = \"{query}\"")));
|
||||
assert!(contents.contains("kind = \"SourceForge\""));
|
||||
assert!(
|
||||
contents.contains("locator = \"https://sourceforge.net/projects/team-app/files/releases\"")
|
||||
);
|
||||
assert!(contents.contains("requested_asset_name = \"team-app-1.0.0.AppImage\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_reports_unsupported_source_queries_distinctly() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("https://gitlab.com/example")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("No remote matches"))
|
||||
.stdout(contains("No installed matches"))
|
||||
.stderr(contains("unsupported source query").not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_reports_supported_sources_without_installable_artifacts_distinctly() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("https://sourceforge.net/projects/team-app/")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("No remote matches"))
|
||||
.stdout(contains("No installed matches"))
|
||||
.stderr(contains("no installable artifact found").not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_emits_live_progress_to_stderr() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("sharkdp/bat")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Installing sharkdp/bat"))
|
||||
.stderr(contains("Resolving source"))
|
||||
.stderr(contains("Discovering release"))
|
||||
.stderr(contains("Selecting artifact"))
|
||||
.stderr(contains("Downloading artifact"))
|
||||
.stderr(contains("Downloaded"))
|
||||
.stderr(contains("Payload Staged"))
|
||||
.stderr(contains("Desktop Entry Written"))
|
||||
.stderr(contains("Icon Extracted"))
|
||||
.stderr(contains("Desktop Integration Refreshed"))
|
||||
.stderr(contains("Registry Saved"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_upm_review_renders_review_heading() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let store = RegistryStore::new(registry_path.clone());
|
||||
store
|
||||
.save(&Registry {
|
||||
version: 1,
|
||||
apps: vec![AppRecord {
|
||||
stable_id: "pingdotgg-t3code".to_owned(),
|
||||
display_name: "t3code".to_owned(),
|
||||
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||
source: None,
|
||||
installed_version: Some("0.0.11".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::User,
|
||||
payload_path: None,
|
||||
desktop_entry_path: None,
|
||||
icon_path: None,
|
||||
}),
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Update Review"))
|
||||
.stdout(contains("apps with updates"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_command_emits_live_progress_to_stderr() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
std::fs::write(
|
||||
®istry_path,
|
||||
"version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["remove", "bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Removing bat"))
|
||||
.stderr(contains("Resolving source: resolving bat"))
|
||||
.stderr(contains("Saving registry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_request_on_immutable_host_falls_back_to_user_install() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let os_release_path = dir.path().join("os-release");
|
||||
std::fs::write(&os_release_path, "ID=fedora\nVARIANT_ID=silverblue\n").unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["--system", "sharkdp/bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env("UPM_OS_RELEASE_PATH", &os_release_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed bat (user)"))
|
||||
.stdout(contains("downgraded to user scope"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_command_applies_updates() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let payload_path = dir
|
||||
.path()
|
||||
.join("install-home/.local/lib/upm/appimages/pingdotgg-t3code.AppImage");
|
||||
let store = RegistryStore::new(registry_path.clone());
|
||||
store
|
||||
.save(&Registry {
|
||||
version: 1,
|
||||
apps: vec![AppRecord {
|
||||
stable_id: "pingdotgg-t3code".to_owned(),
|
||||
display_name: "t3code".to_owned(),
|
||||
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||
source: None,
|
||||
installed_version: Some("0.0.11".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::User,
|
||||
payload_path: None,
|
||||
desktop_entry_path: None,
|
||||
icon_path: None,
|
||||
}),
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("update")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("updated apps: 1"))
|
||||
.stdout(contains("updates found:").not());
|
||||
|
||||
let updated = store.load().unwrap();
|
||||
assert_eq!(updated.apps.len(), 1);
|
||||
assert_eq!(updated.apps[0].stable_id, "pingdotgg-t3code");
|
||||
assert_eq!(updated.apps[0].installed_version.as_deref(), Some("0.0.12"));
|
||||
assert!(payload_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_command_emits_live_progress_to_stderr() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let store = RegistryStore::new(registry_path.clone());
|
||||
store
|
||||
.save(&Registry {
|
||||
version: 1,
|
||||
apps: vec![AppRecord {
|
||||
stable_id: "pingdotgg-t3code".to_owned(),
|
||||
display_name: "t3code".to_owned(),
|
||||
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||
source: None,
|
||||
installed_version: Some("0.0.11".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::User,
|
||||
payload_path: None,
|
||||
desktop_entry_path: None,
|
||||
icon_path: None,
|
||||
}),
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("update")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Updating 1 apps"))
|
||||
.stderr(contains("Resolving source: resolving pingdotgg-t3code"))
|
||||
.stderr(contains("Saving registry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_command_reports_when_previous_installation_is_restored() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let install_home = dir.path().join("install-home");
|
||||
let store = RegistryStore::new(registry_path.clone());
|
||||
let stable_id = "url-example.com-downloads-team-app.appimage";
|
||||
let payload_path = install_home.join(format!(".local/lib/upm/appimages/{stable_id}.AppImage"));
|
||||
|
||||
std::fs::create_dir_all(payload_path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&payload_path, b"previous-payload").unwrap();
|
||||
std::fs::create_dir_all(install_home.join(".local/share")).unwrap();
|
||||
std::fs::write(install_home.join(".local/share/applications"), b"blocker").unwrap();
|
||||
|
||||
store
|
||||
.save(&Registry {
|
||||
version: 1,
|
||||
apps: vec![AppRecord {
|
||||
stable_id: stable_id.to_owned(),
|
||||
display_name: "https://example.com/downloads/team-app.AppImage".to_owned(),
|
||||
source_input: Some("https://example.com/downloads/team-app.AppImage".to_owned()),
|
||||
source: Some(upm_core::domain::source::SourceRef {
|
||||
kind: upm_core::domain::source::SourceKind::DirectUrl,
|
||||
locator: "https://example.com/downloads/team-app.AppImage".to_owned(),
|
||||
input_kind: upm_core::domain::source::SourceInputKind::DirectUrl,
|
||||
normalized_kind: upm_core::domain::source::NormalizedSourceKind::DirectUrl,
|
||||
canonical_locator: None,
|
||||
requested_tag: None,
|
||||
requested_asset_name: None,
|
||||
tracks_latest: false,
|
||||
}),
|
||||
installed_version: Some("unresolved".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::User,
|
||||
payload_path: Some(payload_path.display().to_string()),
|
||||
desktop_entry_path: None,
|
||||
icon_path: None,
|
||||
}),
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.arg("update")
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.env("DISPLAY", ":99")
|
||||
.env("XDG_CURRENT_DESKTOP", "test")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Failed:"))
|
||||
.stdout(contains("restored previous installation"));
|
||||
|
||||
assert_eq!(std::fs::read(&payload_path).unwrap(), b"previous-payload");
|
||||
}
|
||||
234
crates/upm/tests/search_browser.rs
Normal file
234
crates/upm/tests/search_browser.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
use upm::config::SearchConfig;
|
||||
use upm::ui::search_browser::{BrowserPhase, SearchBrowserState, SubmitAction};
|
||||
use upm_core::domain::search::{SearchInstallStatus, SearchResult};
|
||||
|
||||
#[test]
|
||||
fn browser_defaults_to_bottom_to_top_ordering() {
|
||||
let state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
assert_eq!(
|
||||
visible_names(&state),
|
||||
vec!["charlie/app", "bravo/app", "alpha/app"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_moves_cursor_and_pages() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 2);
|
||||
|
||||
state.move_next();
|
||||
assert_eq!(state.cursor_position(), 1);
|
||||
|
||||
state.page_down();
|
||||
assert_eq!(state.cursor_position(), 2);
|
||||
|
||||
state.page_up();
|
||||
assert_eq!(state.cursor_position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_supports_single_and_multiple_numeric_selection() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.apply_numeric_selection("1,3").unwrap();
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app", "alpha/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_supports_numeric_ranges() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.apply_numeric_selection("1-2").unwrap();
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app", "bravo/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_supports_space_separated_numeric_selection() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.apply_numeric_selection("1 3").unwrap();
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app", "alpha/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_numeric_input_updates_selection_immediately() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.push_numeric_input('1');
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app"]);
|
||||
|
||||
state.push_numeric_input(' ');
|
||||
state.push_numeric_input('3');
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app", "alpha/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_numeric_input_keeps_last_good_selection_visible() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.push_numeric_input('1');
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app"]);
|
||||
|
||||
state.push_numeric_input('-');
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app"]);
|
||||
assert_eq!(state.numeric_buffer(), "1-");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_segments_marks_matching_query_fragments() {
|
||||
let fragments = upm::ui::search_browser::highlight_segments("pingdotgg/t3code", "dotgg");
|
||||
|
||||
assert_eq!(fragments.len(), 3);
|
||||
assert_eq!(fragments[1].text, "dotgg");
|
||||
assert!(fragments[1].is_match);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_numeric_selection_preserves_existing_selection() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
state.apply_numeric_selection("2").unwrap();
|
||||
|
||||
let error = state.apply_numeric_selection("2-z").unwrap_err();
|
||||
|
||||
assert!(error.contains("2-z"));
|
||||
assert_eq!(selected_names(&state), vec!["bravo/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmation_requires_selection_before_transition() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
|
||||
assert!(!state.enter_confirmation());
|
||||
assert_eq!(state.phase(), BrowserPhase::Browsing);
|
||||
|
||||
state.toggle_current_selection();
|
||||
assert!(state.enter_confirmation());
|
||||
assert_eq!(state.phase(), BrowserPhase::Confirming);
|
||||
|
||||
state.cancel_confirmation();
|
||||
assert_eq!(state.phase(), BrowserPhase::Browsing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_selection_can_skip_confirmation_from_config() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 3);
|
||||
state.toggle_current_selection();
|
||||
|
||||
let action = state.submit_selection(true);
|
||||
|
||||
assert_eq!(
|
||||
action,
|
||||
SubmitAction::Confirmed(upm::ui::search_browser::SearchSelection {
|
||||
rows: vec![upm::ui::search_browser::SearchRow {
|
||||
status: SearchInstallStatus::Available,
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "charlie/app".to_owned(),
|
||||
description: None,
|
||||
install_query: "charlie/app".to_owned(),
|
||||
version: Some("1.0.0".to_owned()),
|
||||
selectable: true,
|
||||
}],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_rows_are_visible_but_not_selectable() {
|
||||
let mut state = SearchBrowserState::new(installed_first_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.toggle_current_selection();
|
||||
|
||||
assert!(state.selected_rows().is_empty());
|
||||
assert_eq!(
|
||||
state.status_message(),
|
||||
Some("installed result is not selectable")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_rows_remain_selectable() {
|
||||
let mut state = SearchBrowserState::new(update_first_results(), SearchConfig::default(), 3);
|
||||
|
||||
state.toggle_current_selection();
|
||||
|
||||
assert_eq!(selected_names(&state), vec!["charlie/app"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_expression_prefills_from_checklist_selection() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 5);
|
||||
|
||||
state.toggle_current_selection();
|
||||
state.move_to_bottom();
|
||||
state.toggle_current_selection();
|
||||
|
||||
assert_eq!(state.selection_expression(), "1,3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_expression_compacts_adjacent_ranges() {
|
||||
let mut state = SearchBrowserState::new(sample_results(), SearchConfig::default(), 5);
|
||||
|
||||
state.apply_numeric_selection("1-3").unwrap();
|
||||
|
||||
assert_eq!(state.selection_expression(), "1-3");
|
||||
}
|
||||
|
||||
fn sample_results() -> Vec<SearchResult> {
|
||||
vec![
|
||||
sample_result("alpha/app"),
|
||||
sample_result("bravo/app"),
|
||||
sample_result("charlie/app"),
|
||||
]
|
||||
}
|
||||
|
||||
fn sample_result(name: &str) -> SearchResult {
|
||||
SearchResult {
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: name.to_owned(),
|
||||
description: None,
|
||||
source_locator: name.to_owned(),
|
||||
install_query: name.to_owned(),
|
||||
canonical_locator: name.to_owned(),
|
||||
version: Some("1.0.0".to_owned()),
|
||||
install_status: SearchInstallStatus::Available,
|
||||
}
|
||||
}
|
||||
|
||||
fn installed_first_results() -> Vec<SearchResult> {
|
||||
let mut results = sample_results();
|
||||
results[2].install_status = SearchInstallStatus::Installed {
|
||||
installed_version: Some("1.0.0".to_owned()),
|
||||
};
|
||||
results
|
||||
}
|
||||
|
||||
fn update_first_results() -> Vec<SearchResult> {
|
||||
let mut results = sample_results();
|
||||
results[2].install_status = SearchInstallStatus::UpdateAvailable {
|
||||
installed_version: Some("0.9.0".to_owned()),
|
||||
latest_version: Some("1.0.0".to_owned()),
|
||||
};
|
||||
results
|
||||
}
|
||||
|
||||
fn visible_names(state: &SearchBrowserState) -> Vec<&str> {
|
||||
state
|
||||
.ordered_rows()
|
||||
.iter()
|
||||
.map(|row| row.display_name.as_str())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn selected_names(state: &SearchBrowserState) -> Vec<&str> {
|
||||
state
|
||||
.selected_rows()
|
||||
.iter()
|
||||
.map(|row| row.display_name.as_str())
|
||||
.collect()
|
||||
}
|
||||
171
crates/upm/tests/search_cli.rs
Normal file
171
crates/upm/tests/search_cli.rs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
use assert_cmd::Command;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use predicates::str::contains;
|
||||
use tempfile::tempdir;
|
||||
|
||||
const FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
|
||||
|
||||
#[test]
|
||||
fn search_command_renders_remote_github_results() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("Remote Results"))
|
||||
.stdout(contains("[github] sharkdp/bat"))
|
||||
.stdout(contains("Install query: sharkdp/bat"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_renders_local_matches_in_deterministic_order() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
std::fs::write(
|
||||
®istry_path,
|
||||
concat!(
|
||||
"version = 1\n",
|
||||
"[[apps]]\n",
|
||||
"stable_id = \"bat\"\n",
|
||||
"display_name = \"Bat\"\n",
|
||||
"[[apps]]\n",
|
||||
"stable_id = \"bat-tools\"\n",
|
||||
"display_name = \"Bat Tools\"\n",
|
||||
"[[apps]]\n",
|
||||
"stable_id = \"acrobat-reader\"\n",
|
||||
"display_name = \"Acrobat Reader\"\n",
|
||||
"[[apps]]\n",
|
||||
"stable_id = \"combat-viewer\"\n",
|
||||
"display_name = \"Combat Viewer\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed Matches"))
|
||||
.stdout(
|
||||
contains("- Bat (bat)")
|
||||
.and(contains("- Bat Tools (bat-tools)"))
|
||||
.and(contains("- Acrobat Reader (acrobat-reader)"))
|
||||
.and(contains("- Combat Viewer (combat-viewer)")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_is_read_only_for_registry_contents() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let original = "version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n";
|
||||
std::fs::write(®istry_path, original).unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let persisted = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert_eq!(persisted, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_fails_fast_on_malformed_config() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, "[search\nskip_confirmation = true\n").unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env("UPM_CONFIG_PATH", &config_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(config_path.to_string_lossy().as_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_uses_plain_text_output_when_not_on_a_tty() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&config_path,
|
||||
"[search]\nbottom_to_top = false\nskip_confirmation = true\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env("UPM_CONFIG_PATH", &config_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("Remote Results"))
|
||||
.stdout(contains("[github] sharkdp/bat"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_reports_loading_status_to_stderr() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["search", "bat"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Searching bat"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_keeps_empty_results_in_plain_text_mode() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["search", "no-such-app-image-query"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains("No remote matches"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_command_renders_appimagehub_results() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("upm").unwrap();
|
||||
|
||||
cmd.args(["search", "firefox"])
|
||||
.env("UPM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Search Results"))
|
||||
.stdout(contains(
|
||||
"[appimagehub] Firefox by Mozilla - Official AppImage Edition",
|
||||
))
|
||||
.stdout(contains("Install query: appimagehub/2338455"));
|
||||
}
|
||||
553
crates/upm/tests/ui_summary.rs
Normal file
553
crates/upm/tests/ui_summary.rs
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
use upm::DispatchResult;
|
||||
use upm::ui::prompt::render_interaction;
|
||||
use upm::ui::render::{render_dispatch_result, render_update_summary};
|
||||
use upm::ui::search_browser::{SearchRow, format_search_row, render_confirmation_summary};
|
||||
use upm_core::app::add::InstalledApp;
|
||||
use upm_core::app::interaction::{InteractionKind, InteractionRequest};
|
||||
use upm_core::app::list::ListRow;
|
||||
use upm_core::app::remove::{RemovalPlan, RemovalResult};
|
||||
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use upm_core::domain::search::SearchInstallStatus;
|
||||
use upm_core::domain::show::{
|
||||
InstalledShow, MetadataSummary, RemoteArtifactSummary, RemoteShow, ShowResult, SourceSummary,
|
||||
TrackedInstallPaths, UpdateChannelSummary, UpdateStrategySummary,
|
||||
};
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use upm_core::domain::update::ArtifactCandidate;
|
||||
use upm_core::domain::update::{
|
||||
ChannelPreference, ParsedMetadataKind, PlannedUpdate, UpdateChannelKind, UpdatePlan,
|
||||
};
|
||||
use upm_core::integration::install::InstallOutcome;
|
||||
|
||||
fn muted_bold_label(title: &str) -> String {
|
||||
let mut style = upm::ui::theme::current_theme().muted;
|
||||
style.bold = true;
|
||||
upm::ui::theme::apply_style_spec(&format!("{title}:"), &style)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_summary_mentions_selected_count() {
|
||||
let output = render_update_summary(3, 2, 1);
|
||||
assert!(output.contains("selected: 2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_summary_uses_review_heading() {
|
||||
let output = render_update_summary(3, 2, 1);
|
||||
assert!(output.contains("Update Review"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_empty_state_uses_friendlier_copy() {
|
||||
let output = render_dispatch_result(&DispatchResult::List(Vec::new()));
|
||||
assert!(output.contains("No installed apps yet"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_renders_table_with_name_version_and_source() {
|
||||
let output = render_dispatch_result(&DispatchResult::List(vec![ListRow {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "Bat".to_owned(),
|
||||
version: Some("0.25.0".to_owned()),
|
||||
source: "sharkdp/bat".to_owned(),
|
||||
}]));
|
||||
|
||||
assert!(output.contains("Name"));
|
||||
assert!(output.contains("Version"));
|
||||
assert!(output.contains("Source"));
|
||||
assert!(output.contains("Bat"));
|
||||
assert!(output.contains("0.25.0"));
|
||||
assert!(output.contains("sharkdp/bat"));
|
||||
assert!(!output.contains("Bat (bat)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_flow_uses_clearer_summary_labels() {
|
||||
let output = render_dispatch_result(&DispatchResult::UpdatePlan(UpdatePlan {
|
||||
items: vec![PlannedUpdate {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "Bat".to_owned(),
|
||||
selected_channel: ChannelPreference {
|
||||
kind: UpdateChannelKind::GitHubReleases,
|
||||
locator: "sharkdp/bat".to_owned(),
|
||||
reason: "install-origin-match".to_owned(),
|
||||
},
|
||||
selection_reason: "install-origin-match".to_owned(),
|
||||
}],
|
||||
}));
|
||||
|
||||
assert!(output.contains("Update Review"));
|
||||
assert!(output.contains("apps with updates"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removal_summary_lists_removed_files() {
|
||||
let output = render_dispatch_result(&DispatchResult::Removed(Box::new(RemovalResult {
|
||||
removed: RemovalPlan {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "Bat".to_owned(),
|
||||
artifact_paths: vec![
|
||||
"/tmp/install-home/.local/lib/upm/appimages/bat.AppImage".to_owned(),
|
||||
"/tmp/install-home/.local/share/applications/upm-bat.desktop".to_owned(),
|
||||
],
|
||||
},
|
||||
removed_paths: vec![
|
||||
"/tmp/install-home/.local/lib/upm/appimages/bat.AppImage".to_owned(),
|
||||
"/tmp/install-home/.local/share/applications/upm-bat.desktop".to_owned(),
|
||||
],
|
||||
remaining_apps: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
})));
|
||||
|
||||
assert!(output.contains("Removed files"));
|
||||
assert!(output.contains("bat.AppImage"));
|
||||
assert!(output.contains("upm-bat.desktop"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracking_prompt_mentions_requested_and_latest_versions() {
|
||||
let output = render_interaction(&InteractionRequest {
|
||||
key: "tracking-preference".to_owned(),
|
||||
kind: InteractionKind::ChooseTrackingPreference {
|
||||
requested_version: "v0.0.11".to_owned(),
|
||||
latest_version: "v0.0.12".to_owned(),
|
||||
},
|
||||
});
|
||||
|
||||
assert!(output.contains("Choose update tracking"));
|
||||
assert!(output.contains("v0.0.11"));
|
||||
assert!(output.contains("v0.0.12"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracking_prompt_uses_explicit_question_copy() {
|
||||
let output = render_interaction(&InteractionRequest {
|
||||
key: "tracking-preference".to_owned(),
|
||||
kind: InteractionKind::ChooseTrackingPreference {
|
||||
requested_version: "v0.0.11".to_owned(),
|
||||
latest_version: "v0.0.12".to_owned(),
|
||||
},
|
||||
});
|
||||
|
||||
assert!(output.contains("Choose update tracking"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_summary_omits_completed_steps_recap() {
|
||||
let output = render_dispatch_result(&DispatchResult::Added(Box::new(InstalledApp {
|
||||
record: AppRecord {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "bat".to_owned(),
|
||||
source_input: Some("sharkdp/bat".to_owned()),
|
||||
source: None,
|
||||
installed_version: Some("0.25.0".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::User,
|
||||
payload_path: Some(
|
||||
"/tmp/install-home/.local/lib/upm/appimages/sharkdp-bat.AppImage".to_owned(),
|
||||
),
|
||||
desktop_entry_path: Some(
|
||||
"/tmp/install-home/.local/share/applications/upm-sharkdp-bat.desktop"
|
||||
.to_owned(),
|
||||
),
|
||||
icon_path: None,
|
||||
}),
|
||||
},
|
||||
selected_artifact: ArtifactCandidate {
|
||||
url: "https://github.com/sharkdp/bat/releases/download/v0.25.0/bat-x86_64.AppImage"
|
||||
.to_owned(),
|
||||
version: "0.25.0".to_owned(),
|
||||
arch: Some("x86_64".to_owned()),
|
||||
trusted_checksum: None,
|
||||
weak_checksum_md5: None,
|
||||
selection_reason: "heuristic-match".to_owned(),
|
||||
},
|
||||
artifact_size_bytes: 173_015_040,
|
||||
source: SourceRef {
|
||||
kind: SourceKind::GitHub,
|
||||
input_kind: SourceInputKind::RepoShorthand,
|
||||
normalized_kind: NormalizedSourceKind::GitHubRepository,
|
||||
locator: "sharkdp/bat".to_owned(),
|
||||
canonical_locator: Some("sharkdp/bat".to_owned()),
|
||||
requested_tag: None,
|
||||
requested_asset_name: None,
|
||||
tracks_latest: true,
|
||||
},
|
||||
install_scope: InstallScope::User,
|
||||
integration_mode: upm_core::integration::policy::IntegrationMode::Full,
|
||||
install_outcome: InstallOutcome {
|
||||
final_payload_path: "/tmp/install-home/.local/lib/upm/appimages/sharkdp-bat.AppImage"
|
||||
.into(),
|
||||
desktop_entry_path: Some(
|
||||
"/tmp/install-home/.local/share/applications/upm-sharkdp-bat.desktop".into(),
|
||||
),
|
||||
icon_path: None,
|
||||
warnings: Vec::new(),
|
||||
},
|
||||
warnings: Vec::new(),
|
||||
})));
|
||||
|
||||
assert!(output.contains("Installed bat (user)"));
|
||||
assert!(output.contains("Installed files"));
|
||||
assert!(!output.contains("Completed steps"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_browser_row_uses_status_tag_version_and_description_layout() {
|
||||
let row = SearchRow {
|
||||
status: SearchInstallStatus::Installed {
|
||||
installed_version: Some("0.0.12".to_owned()),
|
||||
},
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "pingdotgg/t3code".to_owned(),
|
||||
description: Some("The T3 desktop app.".to_owned()),
|
||||
install_query: "pingdotgg/t3code".to_owned(),
|
||||
version: Some("0.0.12".to_owned()),
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
let output = format_search_row(1, &row, true, true, 120);
|
||||
|
||||
assert!(output.contains('\n'));
|
||||
assert!(output.contains("[installed]"));
|
||||
assert!(output.contains("v0.0.12"));
|
||||
assert!(output.contains("pingdotgg/t3code"));
|
||||
assert!(output.contains("github - The T3 desktop app."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_browser_row_without_description_shows_provider_only() {
|
||||
let row = SearchRow {
|
||||
status: SearchInstallStatus::Available,
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "pingdotgg/t3code".to_owned(),
|
||||
description: None,
|
||||
install_query: "pingdotgg/t3code".to_owned(),
|
||||
version: Some("0.0.12".to_owned()),
|
||||
selectable: true,
|
||||
};
|
||||
|
||||
let output = format_search_row(1, &row, false, false, 120);
|
||||
|
||||
assert!(output.contains("github"));
|
||||
assert!(!output.contains(" - "));
|
||||
assert!(!output.contains("No description available"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_confirmation_summary_lists_selected_rows() {
|
||||
let rows = vec![
|
||||
SearchRow {
|
||||
status: SearchInstallStatus::UpdateAvailable {
|
||||
installed_version: Some("0.0.11".to_owned()),
|
||||
latest_version: Some("0.0.12".to_owned()),
|
||||
},
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "pingdotgg/t3code".to_owned(),
|
||||
description: Some("The T3 desktop app.".to_owned()),
|
||||
install_query: "pingdotgg/t3code".to_owned(),
|
||||
version: Some("0.0.12".to_owned()),
|
||||
selectable: true,
|
||||
},
|
||||
SearchRow {
|
||||
status: SearchInstallStatus::Available,
|
||||
provider_id: "github".to_owned(),
|
||||
display_name: "sharkdp/bat".to_owned(),
|
||||
description: Some("A cat(1) clone with wings.".to_owned()),
|
||||
install_query: "sharkdp/bat".to_owned(),
|
||||
version: Some("1.0.0".to_owned()),
|
||||
selectable: true,
|
||||
},
|
||||
];
|
||||
|
||||
let output = render_confirmation_summary(&rows);
|
||||
|
||||
assert!(output.contains("Confirm Search Selection"));
|
||||
assert!(output.contains("pingdotgg/t3code"));
|
||||
assert!(output.contains("sharkdp/bat"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_show_summary_renders_source_scope_and_paths() {
|
||||
let output = render_dispatch_result(&DispatchResult::Show(Box::new(ShowResult::Installed(
|
||||
InstalledShow {
|
||||
stable_id: "legacy-bat".to_owned(),
|
||||
display_name: "Legacy Bat".to_owned(),
|
||||
installed_version: Some("0.24.0".to_owned()),
|
||||
source_input: Some("sharkdp/bat".to_owned()),
|
||||
source: Some(SourceSummary {
|
||||
kind: SourceKind::GitHub,
|
||||
locator: "https://github.com/sharkdp/bat".to_owned(),
|
||||
canonical_locator: Some("sharkdp/bat".to_owned()),
|
||||
}),
|
||||
install_scope: Some(InstallScope::User),
|
||||
tracked_paths: TrackedInstallPaths {
|
||||
payload_path: Some("/tmp/bat.AppImage".to_owned()),
|
||||
desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()),
|
||||
icon_path: Some("/tmp/upm-bat.png".to_owned()),
|
||||
},
|
||||
update_strategy: Some(UpdateStrategySummary {
|
||||
preferred: UpdateChannelSummary {
|
||||
kind: UpdateChannelKind::GitHubReleases,
|
||||
locator: "sharkdp/bat".to_owned(),
|
||||
reason: "install-origin-match".to_owned(),
|
||||
},
|
||||
alternates: Vec::new(),
|
||||
}),
|
||||
metadata: vec![
|
||||
MetadataSummary {
|
||||
kind: ParsedMetadataKind::ElectronBuilder,
|
||||
version: Some("0.24.0".to_owned()),
|
||||
primary_download: Some("https://example.test/bat.AppImage".to_owned()),
|
||||
checksum: Some("sha256:abcdefghijklmnopqrstuvwxyz0123456789".to_owned()),
|
||||
architecture: Some("x86_64".to_owned()),
|
||||
channel_label: None,
|
||||
warnings: Vec::new(),
|
||||
},
|
||||
MetadataSummary {
|
||||
kind: ParsedMetadataKind::ElectronBuilder,
|
||||
version: Some("0.23.0".to_owned()),
|
||||
primary_download: Some("https://example.test/bat-0.23.0.AppImage".to_owned()),
|
||||
checksum: Some("sha256:efgh".to_owned()),
|
||||
architecture: Some("x86_64".to_owned()),
|
||||
channel_label: None,
|
||||
warnings: Vec::new(),
|
||||
},
|
||||
],
|
||||
},
|
||||
))));
|
||||
|
||||
assert!(output.contains("Legacy Bat (legacy-bat)"));
|
||||
assert!(output.contains("v0.24.0"));
|
||||
assert!(output.contains("[up to date]"));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Source"),
|
||||
upm::ui::theme::muted("github - sharkdp/bat")
|
||||
)));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Update Mechanism"),
|
||||
upm::ui::theme::muted("electron-builder")
|
||||
)));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Architecture"),
|
||||
upm::ui::theme::muted("x86_64")
|
||||
)));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Checksum"),
|
||||
upm::ui::theme::muted("sha256:abcdefg...456789")
|
||||
)));
|
||||
assert!(output.contains(&muted_bold_label("Installed as User")));
|
||||
assert!(output.contains("/tmp/bat.AppImage"));
|
||||
assert!(output.contains("/tmp/upm-bat.desktop"));
|
||||
assert!(!output.contains("[up to date] User"));
|
||||
assert!(!output.contains("past version"));
|
||||
assert!(!output.contains(&upm::ui::theme::label("Metadata")));
|
||||
assert!(!output.contains(&upm::ui::theme::label("Files")));
|
||||
assert!(!output.contains("abcdefghijklmnopqrstuvwxyz0123456789"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_show_summary_reports_when_newer_versions_are_available() {
|
||||
let output = render_dispatch_result(&DispatchResult::Show(Box::new(ShowResult::Installed(
|
||||
InstalledShow {
|
||||
stable_id: "t3code".to_owned(),
|
||||
display_name: "t3code".to_owned(),
|
||||
installed_version: Some("0.0.13".to_owned()),
|
||||
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||
source: Some(SourceSummary {
|
||||
kind: SourceKind::GitHub,
|
||||
locator: "pingdotgg/t3code".to_owned(),
|
||||
canonical_locator: Some("pingdotgg/t3code".to_owned()),
|
||||
}),
|
||||
install_scope: Some(InstallScope::User),
|
||||
tracked_paths: TrackedInstallPaths {
|
||||
payload_path: Some("/tmp/t3code.AppImage".to_owned()),
|
||||
desktop_entry_path: None,
|
||||
icon_path: None,
|
||||
},
|
||||
update_strategy: Some(UpdateStrategySummary {
|
||||
preferred: UpdateChannelSummary {
|
||||
kind: UpdateChannelKind::ElectronBuilder,
|
||||
locator: "https://github.com/pingdotgg/t3code/releases/download/v0.0.16/latest-linux.yml"
|
||||
.to_owned(),
|
||||
reason: "install-origin-match".to_owned(),
|
||||
},
|
||||
alternates: Vec::new(),
|
||||
}),
|
||||
metadata: vec![
|
||||
MetadataSummary {
|
||||
kind: ParsedMetadataKind::ElectronBuilder,
|
||||
version: Some("0.0.16".to_owned()),
|
||||
primary_download: None,
|
||||
checksum: None,
|
||||
architecture: Some("x86_64".to_owned()),
|
||||
channel_label: Some("latest".to_owned()),
|
||||
warnings: Vec::new(),
|
||||
},
|
||||
MetadataSummary {
|
||||
kind: ParsedMetadataKind::ElectronBuilder,
|
||||
version: Some("0.0.15".to_owned()),
|
||||
primary_download: None,
|
||||
checksum: None,
|
||||
architecture: Some("x86_64".to_owned()),
|
||||
channel_label: Some("latest".to_owned()),
|
||||
warnings: Vec::new(),
|
||||
},
|
||||
MetadataSummary {
|
||||
kind: ParsedMetadataKind::ElectronBuilder,
|
||||
version: Some("0.0.14".to_owned()),
|
||||
primary_download: None,
|
||||
checksum: None,
|
||||
architecture: Some("x86_64".to_owned()),
|
||||
channel_label: Some("latest".to_owned()),
|
||||
warnings: Vec::new(),
|
||||
},
|
||||
MetadataSummary {
|
||||
kind: ParsedMetadataKind::ElectronBuilder,
|
||||
version: Some("0.0.13".to_owned()),
|
||||
primary_download: None,
|
||||
checksum: None,
|
||||
architecture: Some("x86_64".to_owned()),
|
||||
channel_label: Some("latest".to_owned()),
|
||||
warnings: Vec::new(),
|
||||
},
|
||||
],
|
||||
},
|
||||
))));
|
||||
|
||||
assert!(output.contains("t3code (t3code)"));
|
||||
assert!(output.contains("v0.0.13"));
|
||||
assert!(output.contains("[update available]"));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Source"),
|
||||
upm::ui::theme::muted("github - pingdotgg/t3code")
|
||||
)));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Update Mechanism"),
|
||||
upm::ui::theme::muted("electron-builder")
|
||||
)));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Architecture"),
|
||||
upm::ui::theme::muted("x86_64")
|
||||
)));
|
||||
assert!(output.contains(&muted_bold_label("Installed as User")));
|
||||
assert!(!output.contains("[update available] User"));
|
||||
assert!(!output.contains("past versions"));
|
||||
assert!(!output.contains("latest v0.0.16"));
|
||||
assert!(!output.contains(&upm::ui::theme::label("Metadata")));
|
||||
assert!(!output.contains(&upm::ui::theme::label("Files")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_show_list_renders_each_app_using_singular_show_format() {
|
||||
let output = render_dispatch_result(&DispatchResult::ShowAll(vec![
|
||||
InstalledShow {
|
||||
stable_id: "legacy-bat".to_owned(),
|
||||
display_name: "Legacy Bat".to_owned(),
|
||||
installed_version: Some("0.24.0".to_owned()),
|
||||
source_input: Some("sharkdp/bat".to_owned()),
|
||||
source: Some(SourceSummary {
|
||||
kind: SourceKind::GitHub,
|
||||
locator: "https://github.com/sharkdp/bat".to_owned(),
|
||||
canonical_locator: Some("sharkdp/bat".to_owned()),
|
||||
}),
|
||||
install_scope: Some(InstallScope::User),
|
||||
tracked_paths: TrackedInstallPaths {
|
||||
payload_path: Some("/tmp/bat.AppImage".to_owned()),
|
||||
desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()),
|
||||
icon_path: None,
|
||||
},
|
||||
update_strategy: None,
|
||||
metadata: vec![MetadataSummary {
|
||||
kind: ParsedMetadataKind::ElectronBuilder,
|
||||
version: Some("0.24.0".to_owned()),
|
||||
primary_download: None,
|
||||
checksum: Some("sha256:abcdefghijklmnopqrstuvwxyz0123456789".to_owned()),
|
||||
architecture: Some("x86_64".to_owned()),
|
||||
channel_label: None,
|
||||
warnings: Vec::new(),
|
||||
}],
|
||||
},
|
||||
InstalledShow {
|
||||
stable_id: "t3code".to_owned(),
|
||||
display_name: "t3code".to_owned(),
|
||||
installed_version: Some("0.0.13".to_owned()),
|
||||
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||
source: Some(SourceSummary {
|
||||
kind: SourceKind::GitHub,
|
||||
locator: "pingdotgg/t3code".to_owned(),
|
||||
canonical_locator: Some("pingdotgg/t3code".to_owned()),
|
||||
}),
|
||||
install_scope: Some(InstallScope::User),
|
||||
tracked_paths: TrackedInstallPaths {
|
||||
payload_path: Some("/tmp/t3code.AppImage".to_owned()),
|
||||
desktop_entry_path: None,
|
||||
icon_path: None,
|
||||
},
|
||||
update_strategy: None,
|
||||
metadata: vec![MetadataSummary {
|
||||
kind: ParsedMetadataKind::ElectronBuilder,
|
||||
version: Some("0.0.16".to_owned()),
|
||||
primary_download: None,
|
||||
checksum: None,
|
||||
architecture: Some("x86_64".to_owned()),
|
||||
channel_label: None,
|
||||
warnings: Vec::new(),
|
||||
}],
|
||||
},
|
||||
]));
|
||||
|
||||
assert!(output.contains("Legacy Bat (legacy-bat)"));
|
||||
assert!(output.contains("t3code (t3code)"));
|
||||
assert!(output.contains("\n\n"));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Source"),
|
||||
upm::ui::theme::muted("github - sharkdp/bat")
|
||||
)));
|
||||
assert!(output.contains(&format!(
|
||||
"{} {}",
|
||||
muted_bold_label("Source"),
|
||||
upm::ui::theme::muted("github - pingdotgg/t3code")
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_show_summary_renders_source_artifact_and_reason() {
|
||||
let output = render_dispatch_result(&DispatchResult::Show(Box::new(ShowResult::Remote(
|
||||
RemoteShow {
|
||||
source: SourceSummary {
|
||||
kind: SourceKind::GitHub,
|
||||
locator: "sharkdp/bat".to_owned(),
|
||||
canonical_locator: Some("sharkdp/bat".to_owned()),
|
||||
},
|
||||
artifact: RemoteArtifactSummary {
|
||||
url: "https://github.com/sharkdp/bat/releases/download/v1.0.0/Bat-1.0.0-x86_64.AppImage"
|
||||
.to_owned(),
|
||||
version: Some("1.0.0".to_owned()),
|
||||
arch: Some("x86_64".to_owned()),
|
||||
trusted_checksum: Some("sha512:abcd".to_owned()),
|
||||
selection_reason: "metadata-guided".to_owned(),
|
||||
},
|
||||
interactions: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
},
|
||||
))));
|
||||
|
||||
assert!(output.contains("Resolved Source"));
|
||||
assert!(output.contains("github"));
|
||||
assert!(output.contains("sharkdp/bat"));
|
||||
assert!(output.contains("Bat-1.0.0-x86_64.AppImage"));
|
||||
assert!(output.contains("1.0.0"));
|
||||
assert!(output.contains("metadata-guided"));
|
||||
assert!(output.contains("sha512:abcd"));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue