mirror of
https://github.com/Felitendo/spotifast.git
synced 2026-11-01 09:23:26 +01:00
243 lines
8.1 KiB
Rust
243 lines
8.1 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
const COMMAND: &str = env!("CARGO_BIN_EXE_spotifast");
|
|
|
|
struct Scratch(PathBuf);
|
|
|
|
impl Scratch {
|
|
fn new() -> Self {
|
|
let path =
|
|
std::env::temp_dir().join(format!("spotifast-branding-{:016x}", rand::random::<u64>()));
|
|
std::fs::create_dir(&path).unwrap();
|
|
Self(path)
|
|
}
|
|
}
|
|
|
|
impl Drop for Scratch {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_command_reports_its_name_and_passes_the_update_version_check() {
|
|
let output = Command::new(COMMAND).arg("--version").output().unwrap();
|
|
assert!(output.status.success());
|
|
assert_eq!(
|
|
String::from_utf8(output.stdout).unwrap().trim(),
|
|
format!("spotifast {}", env!("CARGO_PKG_VERSION"))
|
|
);
|
|
let help = Command::new(COMMAND).arg("--help").output().unwrap();
|
|
assert!(help.status.success());
|
|
assert!(
|
|
String::from_utf8(help.stdout)
|
|
.unwrap()
|
|
.contains("Usage: spotifast")
|
|
);
|
|
assert_eq!(spotifast::updates::CONFIG.slug, "spotifast");
|
|
}
|
|
|
|
/// The app's name before the rename is gone from everything but the past
|
|
/// release notes and the two old guide addresses that still redirect.
|
|
/// Spelled in two halves so this file does not match itself.
|
|
#[test]
|
|
fn no_file_carries_the_old_name() {
|
|
let old = ["fast", "potify"].concat();
|
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
let allowed = [
|
|
root.join("packaging/release-notes"),
|
|
// Old guide URLs still redirect, so links from elsewhere keep working.
|
|
root.join("docs/_guide/using-spotifast.md"),
|
|
root.join("docs/_guide/what-is-spotifast.md"),
|
|
];
|
|
let skipped = [
|
|
"target",
|
|
"_site",
|
|
".jekyll-cache",
|
|
"vendor",
|
|
".bundle",
|
|
".git",
|
|
".claude",
|
|
".cache",
|
|
"dist",
|
|
];
|
|
let mut found = Vec::new();
|
|
let mut pending = vec![root.to_path_buf()];
|
|
while let Some(dir) = pending.pop() {
|
|
for entry in std::fs::read_dir(&dir).unwrap() {
|
|
let path = entry.unwrap().path();
|
|
let name = path.file_name().unwrap().to_string_lossy().into_owned();
|
|
if allowed.contains(&path) || skipped.contains(&name.as_str()) {
|
|
continue;
|
|
}
|
|
if path.is_dir() {
|
|
pending.push(path);
|
|
continue;
|
|
}
|
|
let relative = path
|
|
.strip_prefix(root)
|
|
.unwrap()
|
|
.to_string_lossy()
|
|
.into_owned();
|
|
let text = String::from_utf8_lossy(&std::fs::read(&path).unwrap()).to_lowercase();
|
|
if relative.to_lowercase().contains(&old) || text.contains(&old) {
|
|
found.push(relative);
|
|
}
|
|
}
|
|
}
|
|
found.sort();
|
|
assert!(found.is_empty(), "the old name is still in {found:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn existing_preferences_and_custom_connect_names_survive_a_save() {
|
|
use spotifast::settings::{Settings, ThemeChoice};
|
|
|
|
let scratch = Scratch::new();
|
|
let path = scratch.0.join("settings.json");
|
|
for name in ["Spotifast", "Living room", "Carmine's laptop"] {
|
|
let saved = Settings {
|
|
device_name: name.into(),
|
|
theme: ThemeChoice::Light,
|
|
volume: 37,
|
|
pinned_contexts: vec![
|
|
"spotify:playlist:123".into(),
|
|
spotifast::settings::LIKED_SONGS_KEY.into(),
|
|
],
|
|
..Settings::default()
|
|
};
|
|
saved.save(&path);
|
|
assert_eq!(Settings::load(&path), saved);
|
|
}
|
|
assert_eq!(Settings::default().device_name, "Spotifast");
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn the_command_forwards_links_to_the_existing_instance_on_a_private_bus() {
|
|
use spotifast::single_instance::{ControlCommand, Outcome};
|
|
|
|
const CHILD: &str = "SPOTIFAST_BRANDING_PRIVATE_BUS";
|
|
if std::env::var_os(CHILD).is_none() {
|
|
// A clean build (including Nix) need not have /etc/dbus-1/session.conf.
|
|
// Own the bus configuration too, without loading desktop services.
|
|
let scratch = Scratch::new();
|
|
let config = scratch.0.join("session.conf");
|
|
std::fs::write(
|
|
&config,
|
|
r#"<busconfig>
|
|
<type>session</type>
|
|
<listen>unix:tmpdir=/tmp</listen>
|
|
<auth>EXTERNAL</auth>
|
|
<policy context="default">
|
|
<allow own="*"/>
|
|
<allow send_destination="*"/>
|
|
<allow receive_sender="*"/>
|
|
</policy>
|
|
</busconfig>"#,
|
|
)
|
|
.unwrap();
|
|
let result = Command::new("dbus-run-session")
|
|
.arg("--config-file")
|
|
.arg(config)
|
|
.args(["--"])
|
|
.arg(std::env::current_exe().unwrap())
|
|
.args([
|
|
"--exact",
|
|
"the_command_forwards_links_to_the_existing_instance_on_a_private_bus",
|
|
"--nocapture",
|
|
])
|
|
.env(CHILD, "1")
|
|
// The running copy's slot lives in the runtime directory, so a
|
|
// Spotifast already running on this machine is left alone.
|
|
.env("XDG_RUNTIME_DIR", &scratch.0)
|
|
.output()
|
|
.expect("the Linux test environment needs dbus-run-session");
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
return;
|
|
}
|
|
|
|
let scratch = Scratch::new();
|
|
let result = Command::new(COMMAND).arg("reload-themes").output().unwrap();
|
|
assert_eq!(result.status.code(), Some(1));
|
|
assert!(String::from_utf8_lossy(&result.stderr).contains("not running"));
|
|
let result = Command::new(COMMAND).arg("like").output().unwrap();
|
|
assert_eq!(result.status.code(), Some(1));
|
|
assert!(
|
|
String::from_utf8_lossy(&result.stderr).contains("not running"),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
let Outcome::Only(guard) = spotifast::single_instance::acquire(&Default::default(), None)
|
|
else {
|
|
panic!("the private bus must start without another instance");
|
|
};
|
|
let commands = guard.commands();
|
|
for (link, uri) in [
|
|
(
|
|
"spotify:track:4uLU6hMCjMI75M1A2tKUQC",
|
|
"spotify:track:4uLU6hMCjMI75M1A2tKUQC",
|
|
),
|
|
(
|
|
"https://open.spotify.com/search/here%20comes%20the%20sun",
|
|
"spotify:search:here%20comes%20the%20sun",
|
|
),
|
|
(
|
|
"spotify://search/%E6%9D%B1%E4%BA%AC",
|
|
"spotify:search:%E6%9D%B1%E4%BA%AC",
|
|
),
|
|
] {
|
|
let mut child = Command::new(COMMAND)
|
|
.arg(link)
|
|
.env("XDG_CONFIG_HOME", scratch.0.join("config"))
|
|
.env("XDG_STATE_HOME", scratch.0.join("state"))
|
|
.env("XDG_CACHE_HOME", scratch.0.join("cache"))
|
|
.spawn()
|
|
.unwrap();
|
|
let started = std::time::Instant::now();
|
|
let status = loop {
|
|
if let Some(status) = child.try_wait().unwrap() {
|
|
break status;
|
|
}
|
|
if started.elapsed() > std::time::Duration::from_secs(10) {
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
panic!("a second command did not forward its link and exit");
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
};
|
|
assert!(status.success());
|
|
assert_eq!(
|
|
std::mem::take(&mut *commands.lock().unwrap()),
|
|
vec![ControlCommand::OpenLink(uri.into())]
|
|
);
|
|
}
|
|
let result = Command::new(COMMAND).arg("reload-themes").output().unwrap();
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
assert_eq!(
|
|
std::mem::take(&mut *commands.lock().unwrap()),
|
|
vec![ControlCommand::ReloadThemes],
|
|
"a reload only asks for themes, never OpenLink or Show"
|
|
);
|
|
let result = Command::new(COMMAND).arg("like").output().unwrap();
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
assert_eq!(
|
|
std::mem::take(&mut *commands.lock().unwrap()),
|
|
vec![ControlCommand::ToggleSaved],
|
|
"like only toggles the saved state, never OpenLink or Show"
|
|
);
|
|
}
|