use crate::context::Context;
use crate::error::{Error, Result};
use crate::host::Scheduler;
use crate::service::Permit;
use std::path::{Path, PathBuf};
pub(crate) const EVERY_SECONDS: u32 = 86_400;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Installed {
Yes {
location: Location,
every_seconds: u32,
},
No,
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Location {
File(PathBuf),
Task(String),
}
impl std::fmt::Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Location::File(path) => write!(f, "{}", path.display()),
Location::Task(task) => f.write_str(task),
}
}
}
fn scheduler(ctx: &Context) -> Option<&dyn Scheduler> {
ctx.host().scheduler()
}
#[cfg(test)]
pub(crate) fn path(ctx: &Context) -> Option<PathBuf> {
Some(match scheduler(ctx)?.location(ctx) {
Location::File(path) => path,
Location::Task(task) => crate::host::task::pretend_file(ctx, &task),
})
}
pub(crate) fn status(ctx: &Context) -> Installed {
match scheduler(ctx) {
None => Installed::Unsupported,
Some(s) if s.installed(ctx) => Installed::Yes {
location: s.location(ctx),
every_seconds: EVERY_SECONDS,
},
Some(_) => Installed::No,
}
}
pub const SCHEDULED_RUN: [&str; 2] = ["renew", "--scheduled"];
pub(crate) fn serves(ctx: &Context) -> bool {
ctx.os
.same_path(&crate::home::dir(ctx), &ctx.default_pitboard_home())
}
pub(crate) fn started_this_run(ctx: &Context) -> bool {
ctx.marked_as_scheduled()
|| scheduler(ctx).is_some_and(|s| s.started_this_process(ctx.scheduled_job()))
}
pub(crate) fn for_its_run(ctx: &Context) -> Context {
if !started_this_run(ctx) {
return ctx.clone();
}
#[cfg(any(test, feature = "test-support"))]
assert!(
!crate::host::user::is_the_accounts_own_home(ctx.home()),
"a build for tests ran as the daily renewal schedule with this account's own home, \
where it would renew the real Pitboard directory: give it a home of its own"
);
let run = ctx.clone().with_pitboard_home(ctx.default_pitboard_home());
match scheduler(&run).map(|s| s.read_by_its_runs(&run)) {
Some(given) if !given.is_empty() => run.with_proxy_variables_over(&given),
_ => run,
}
}
fn program(ctx: &Context) -> Result<PathBuf> {
program_of(ctx.schedule_program(), crate::host::current_program)
}
fn program_of(
named: Option<&Path>,
running: impl FnOnce() -> std::io::Result<PathBuf>,
) -> Result<PathBuf> {
let program = match named {
Some(named) => named.to_path_buf(),
None => running().map_err(|source| Error::HomeUnwritable {
path: PathBuf::from("the running Pitboard"),
source,
})?,
};
lasting(&program)?;
Ok(program)
}
fn lasting(program: &Path) -> Result<&Path> {
if in_a_temporary_copy(program) {
return Err(Error::ScheduleProgramTemporary {
path: program.to_path_buf(),
});
}
if !program.is_file() {
return Err(Error::ScheduleProgramMissing {
path: program.to_path_buf(),
});
}
Ok(program)
}
pub fn in_a_temporary_copy(program: &Path) -> bool {
program
.components()
.any(|part| part.as_os_str() == "AppTranslocation")
|| opened_from_a_zip(program)
}
pub(crate) fn opened_from_a_zip(program: &Path) -> bool {
program.to_string_lossy().split(['/', '\\']).any(|part| {
let name = part.to_ascii_lowercase();
let Some(numbered) = name.strip_prefix("temp") else {
return false;
};
let digits = numbered.bytes().take_while(u8::is_ascii_digit).count();
digits > 0 && numbered[digits..].starts_with('_') && name.ends_with(".zip")
})
}
pub(crate) fn task(ctx: &Context) -> Option<crate::host::task::Task> {
scheduler(ctx)?.task(ctx)
}
pub(crate) fn installed_program(ctx: &Context) -> Option<PathBuf> {
scheduler(ctx)?.program(ctx)
}
fn environment(
ctx: &Context,
proxy: impl IntoIterator<Item = (String, String)>,
) -> Vec<(String, String)> {
let refusing = (!ctx.argv_fallback()).then(|| ("PITBOARD_NO_ARGV".into(), "1".into()));
refusing.into_iter().chain(proxy).collect()
}
fn installed_proxy_variables(ctx: &Context) -> Vec<(String, String)> {
let Some(scheduler) = scheduler(ctx) else {
return Vec::new();
};
scheduler
.environment(ctx)
.into_iter()
.filter(|(name, _)| {
crate::proxy::NAMING
.iter()
.chain(&crate::proxy::EXEMPTING)
.any(|proxy| proxy == name)
})
.collect()
}
pub(crate) fn installed_proxies(ctx: &Context) -> Option<crate::proxy::Proxies> {
if !scheduler(ctx)?.installed(ctx) {
return None;
}
let given: crate::context::Environment = installed_proxy_variables(ctx).into_iter().collect();
Some(crate::proxy::Proxies::read(&given))
}
pub(crate) fn install(ctx: &Context, permit: Permit) -> Result<Location> {
if !serves(ctx) {
return Err(Error::ScheduleNotDefaultHome {
home: crate::home::dir(ctx),
default: ctx.default_pitboard_home(),
});
}
let proxy = ctx
.proxy
.variables()
.iter()
.map(|(name, value)| ((*name).to_string(), value.clone()));
let location = put(ctx, permit, &program(ctx)?, &environment(ctx, proxy))?;
crate::audit::record(ctx, permit, "schedule", "install", "ok");
Ok(location)
}
pub(crate) fn repair(ctx: &Context, permit: Permit) -> Result<bool> {
if scheduler(ctx).is_none() || started_this_run(ctx) {
return Ok(false);
}
let Some(named) = ctx.schedule_program() else {
return Ok(false);
};
if !serves(ctx)
|| lasting(named).is_err()
|| !installed_program(ctx).is_some_and(|program| broken(ctx, &program))
{
return Ok(false);
}
let kept = installed_proxy_variables(ctx);
let repaired = put(ctx, permit, named, &environment(ctx, kept));
crate::audit::record(
ctx,
permit,
"schedule",
"repair",
match &repaired {
Ok(_) => "ok",
Err(e) => e.code(),
},
);
repaired.map(|_| true)
}
fn broken(ctx: &Context, program: &Path) -> bool {
match task(ctx) {
Some(task) if !task.drifted.is_empty() => false,
Some(_) => an_apps_own_program(program) || !program.is_file(),
None => an_apps_own_program(program),
}
}
pub(crate) fn an_apps_own_program(program: &Path) -> bool {
let mut dirs = program
.ancestors()
.skip(1)
.map(|dir| dir.file_name().and_then(|n| n.to_str()));
dirs.next() == Some(Some("MacOS")) && dirs.next() == Some(Some("Contents"))
}
fn put(
ctx: &Context,
permit: Permit,
program: &Path,
environment: &[(String, String)],
) -> Result<Location> {
let Some(scheduler) = scheduler(ctx) else {
return Err(Error::ScheduleUnsupported);
};
scheduler.put(ctx, permit, program, environment)?;
Ok(scheduler.location(ctx))
}
pub(crate) fn uninstall(ctx: &Context, permit: Permit) -> Result<bool> {
let Some(scheduler) = scheduler(ctx) else {
return Err(Error::ScheduleUnsupported);
};
if !scheduler.remove(ctx, permit)? {
return Ok(false);
}
crate::audit::record(ctx, permit, "schedule", "uninstall", "ok");
Ok(true)
}
#[cfg(test)]
#[allow(
clippy::disallowed_methods,
reason = "a test sets up its scratch files as a person or a tool would"
)]
mod tests {
use super::*;
use crate::host::Os;
use crate::host::memory::MemoryHost;
use std::sync::Arc;
struct Scratch(tempfile::TempDir, Arc<MemoryHost>);
impl Scratch {
fn new() -> Scratch {
Scratch(
tempfile::tempdir().expect("a scratch home"),
MemoryHost::new(),
)
}
fn path(&self) -> &Path {
self.0.path()
}
fn ctx(&self) -> Context {
Context::new(self.path().to_path_buf()).with_memory_stores(Arc::clone(&self.1))
}
fn read(&self, vars: &[(&str, &str)]) -> Context {
let home = self.path().to_string_lossy().into_owned();
let env: crate::context::Environment =
[("HOME", home.as_str()), ("USERPROFILE", home.as_str())]
.iter()
.chain(vars)
.copied()
.collect();
Context::for_command_line(&env).with_memory_stores(Arc::clone(&self.1))
}
}
fn given(ctx: &Context) -> Vec<(String, String)> {
scheduler(ctx).expect("a scheduler").environment(ctx)
}
fn pairs(given: &[(&str, &str)]) -> Vec<(String, String)> {
given
.iter()
.map(|(name, value)| ((*name).to_string(), (*value).to_string()))
.collect()
}
fn a_program_at(path: &Path) {
std::fs::create_dir_all(path.parent().expect("a directory")).expect("its directory");
std::fs::write(path, "").expect("a program");
}
#[test]
fn a_schedule_the_scheduler_will_not_start_is_not_left_on_disk() {
let home = Scratch::new();
let program = home.path().join("bin/pitboard");
a_program_at(&program);
let ctx = home.ctx().with_schedule_program(program);
home.1.refuse_next_start();
let refused = install(&ctx, Permit::for_a_test()).expect_err("refused");
assert_eq!(refused.code(), "schedule_refused");
assert_eq!(status(&ctx), Installed::No);
assert_eq!(installed_program(&ctx), None);
}
#[test]
fn a_repair_the_scheduler_will_not_start_leaves_the_schedule_as_it_was() {
let home = Scratch::new();
std::fs::create_dir_all(home.ctx().default_pitboard_home()).expect("a Pitboard home");
let app = home
.path()
.join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
let bundled = home
.path()
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&app);
a_program_at(&bundled);
let ctx = home.ctx();
install(
&ctx.clone().with_schedule_program(app.clone()),
Permit::for_a_test(),
)
.expect("0.3.0's schedule");
home.1.refuse_next_start();
let refused = repair(
&ctx.clone().with_schedule_program(bundled),
Permit::for_a_test(),
)
.expect_err("refused");
assert_eq!(refused.code(), "schedule_refused");
assert!(matches!(status(&ctx), Installed::Yes { .. }));
assert_eq!(installed_program(&ctx), Some(app));
}
#[test]
fn nothing_is_installed_on_a_machine_where_nothing_was_installed() {
let home = Scratch::new();
let ctx = home.ctx();
assert_eq!(status(&ctx), Installed::No);
assert_eq!(installed_program(&ctx), None);
}
#[test]
fn a_machine_with_no_scheduler_says_so() {
let home = Scratch::new();
home.1.without_a_scheduler();
let program = home.path().join("bin/pitboard");
a_program_at(&program);
let ctx = home.ctx().with_schedule_program(program);
assert_eq!(status(&ctx), Installed::Unsupported);
assert_eq!(path(&ctx), None);
assert_eq!(
install(&ctx, Permit::for_a_test())
.expect_err("nowhere")
.code(),
"schedule_unsupported"
);
assert_eq!(
uninstall(&ctx, Permit::for_a_test())
.expect_err("nowhere")
.code(),
"schedule_unsupported"
);
assert!(!repair(&ctx, Permit::for_a_test()).expect("nothing to do"));
}
#[test]
fn the_schedule_runs_the_pitboard_the_context_names_and_otherwise_this_one() {
let home = Scratch::new();
let ctx = home.ctx();
let scheduled = program(&ctx).expect("this program");
let running = std::env::current_exe().expect("this test's own program");
assert_eq!(
std::fs::canonicalize(&scheduled).expect("there"),
std::fs::canonicalize(&running).expect("there"),
"the command line schedules itself"
);
let bundled = home
.path()
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&bundled);
assert_eq!(
program(&ctx.with_schedule_program(bundled.clone())).expect("the named one"),
bundled,
"an app schedules the command line it comes with"
);
}
#[test]
fn a_named_pitboard_that_will_not_be_there_is_refused() {
let home = Scratch::new();
let ctx = home.ctx();
let missing = home.path().join("Pitboard.app/Contents/Helpers/pitboard");
let refused = install(
&ctx.clone().with_schedule_program(missing.clone()),
Permit::for_a_test(),
)
.expect_err("nothing there to run");
assert_eq!(refused.code(), "schedule_program_missing");
assert!(
refused.to_string().contains(&missing.display().to_string()),
"{refused}"
);
let temporary = home
.path()
.join("AppTranslocation/6A1C/d/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&temporary);
let refused = install(
&ctx.clone().with_schedule_program(temporary.clone()),
Permit::for_a_test(),
)
.expect_err("a copy that goes away");
assert_eq!(refused.code(), "schedule_program_temporary");
assert!(
refused
.to_string()
.contains(&temporary.display().to_string())
&& refused.to_string().contains("Applications folder"),
"{refused}"
);
let unzipped = home
.path()
.join("Temp1_pitboard-v0.15.0-x86_64-pc-windows-msvc.zip")
.join("pitboard.exe");
a_program_at(&unzipped);
let refused = install(
&ctx.clone().with_schedule_program(unzipped),
Permit::for_a_test(),
)
.expect_err("a copy Explorer made of a zip");
assert_eq!(refused.code(), "schedule_program_temporary");
assert!(
refused
.to_string()
.contains("Install Pitboard, or extract the zip"),
"{refused}"
);
assert_eq!(status(&ctx), Installed::No, "nothing was written");
}
#[test]
fn the_running_pitboard_is_refused_where_a_named_one_would_be() {
let home = Scratch::new();
let unzipped = home
.path()
.join("Temp1_pitboard-v0.15.0-x86_64-pc-windows-msvc.zip")
.join("pitboard.exe");
a_program_at(&unzipped);
let refused = program_of(None, || Ok(unzipped.clone())).expect_err("a copy that goes");
assert_eq!(refused.code(), "schedule_program_temporary");
let gone = home.path().join("gone").join("pitboard");
let refused = program_of(None, || Ok(gone.clone())).expect_err("nothing there");
assert_eq!(refused.code(), "schedule_program_missing");
let installed = home.path().join("bin").join("pitboard");
a_program_at(&installed);
assert_eq!(
program_of(None, || Ok(installed.clone())).expect("one that lasts"),
installed
);
}
#[test]
fn a_temporary_copy_is_told_by_its_path() {
for (path, temporary) in [
(
"/private/var/folders/xy/abc/T/AppTranslocation/0A1B2C/d/Pitboard.app/Contents/\
Helpers/pitboard",
true,
),
(
"/Applications/Pitboard.app/Contents/Helpers/pitboard",
false,
),
(
"/Users/x/MyAppTranslocation/Pitboard.app/Contents/Helpers/pitboard",
false,
),
(
"/Users/x/AppTranslocations/Pitboard.app/Contents/Helpers/pitboard",
false,
),
(
r"C:\Users\x\AppData\Local\Temp\Temp1_pitboard.zip\pitboard\pitboard.exe",
true,
),
(r"C:\Users\x\Temp12_Pitboard.ZIP\bin\pitboard.exe", true),
(r"C:\Users\x\Temp_pitboard.zip\pitboard.exe", false),
(r"C:\Users\x\Temp1_pitboard\pitboard.exe", false),
] {
assert_eq!(in_a_temporary_copy(Path::new(path)), temporary, "{path}");
}
}
#[test]
fn an_installed_schedule_says_which_pitboard_it_runs() {
let home = Scratch::new();
let bundled = home
.path()
.join("Tools&Apps/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&bundled);
let ctx = home.ctx().with_schedule_program(bundled.clone());
install(&ctx, Permit::for_a_test()).expect("installed");
assert!(matches!(status(&ctx), Installed::Yes { .. }));
assert_eq!(installed_program(&ctx), Some(bundled));
assert!(uninstall(&ctx, Permit::for_a_test()).expect("taken away"));
assert_eq!(status(&ctx), Installed::No);
assert_eq!(installed_program(&ctx), None);
assert!(!uninstall(&ctx, Permit::for_a_test()).expect("nothing there"));
}
#[test]
fn a_schedule_that_runs_an_app_is_pointed_at_its_command_line() {
let home = Scratch::new();
std::fs::create_dir_all(home.ctx().default_pitboard_home()).expect("a Pitboard home");
let app = home
.path()
.join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
let bundled = home
.path()
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&app);
a_program_at(&bundled);
let ctx = home.ctx();
let the_app = ctx.clone().with_schedule_program(bundled.clone());
assert!(
!repair(&the_app, Permit::for_a_test()).expect("nothing to do"),
"nothing installed"
);
install(
&ctx.clone().with_schedule_program(app.clone()),
Permit::for_a_test(),
)
.expect("0.3.0's schedule");
assert!(
!repair(&ctx, Permit::for_a_test()).expect("nothing to do"),
"no command line named"
);
let gone = home.path().join("Old.app/Contents/Helpers/pitboard");
assert!(
!repair(
&ctx.clone().with_schedule_program(gone),
Permit::for_a_test()
)
.expect("nothing to do"),
"a command line that is not there"
);
assert!(
!repair(
&the_app
.clone()
.with_pitboard_home(home.path().join("elsewhere")),
Permit::for_a_test()
)
.expect("nothing to do"),
"a schedule another home's Pitboard looks after"
);
assert_eq!(installed_program(&ctx), Some(app.clone()));
assert!(repair(&the_app, Permit::for_a_test()).expect("repaired"));
assert_eq!(installed_program(&ctx), Some(bundled));
let logged = crate::audit::read(&ctx, 1);
assert_eq!(
logged
.iter()
.map(|e| (e.verb.as_str(), e.subject.as_str(), e.outcome.as_str()))
.collect::<Vec<_>>(),
[("schedule", "repair", "ok")]
);
assert!(
!repair(&the_app, Permit::for_a_test()).expect("nothing to do"),
"a schedule that runs a command line already"
);
install(
&ctx.clone().with_schedule_program(app.clone()),
Permit::for_a_test(),
)
.expect("the app again");
let temporary = home
.path()
.join("AppTranslocation/6A1C/d/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&temporary);
assert!(
!repair(
&ctx.clone().with_schedule_program(temporary),
Permit::for_a_test()
)
.expect("nothing to do"),
"a command line that is gone once the app quits"
);
assert_eq!(installed_program(&ctx), Some(app));
}
#[test]
fn a_process_another_job_started_still_repairs() {
let home = Scratch::new();
std::fs::create_dir_all(home.ctx().default_pitboard_home()).expect("a Pitboard home");
let app = home
.path()
.join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
let bundled = home
.path()
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&app);
a_program_at(&bundled);
let ctx = home.ctx();
install(
&ctx.clone().with_schedule_program(app),
Permit::for_a_test(),
)
.expect("0.3.0's schedule");
let opened = ctx
.clone()
.with_schedule_program(bundled.clone())
.with_scheduled_job("application.com.datlechin.pitboard.1.2".into());
assert!(repair(&opened, Permit::for_a_test()).expect("repaired"));
assert_eq!(installed_program(&ctx), Some(bundled));
}
#[test]
fn the_app_points_a_task_whose_pitboard_is_gone_at_its_own_and_nothing_else() {
let home = Scratch(
tempfile::tempdir().expect("a scratch home"),
MemoryHost::scheduling_tasks("S-1-5-21-1"),
);
let ctx = home.read(&[("HTTPS_PROXY", "http://proxy.example.com:3128")]);
let scoop = home
.path()
.join(r"scoop\apps\pitboard\current\pitboard.exe");
let bundled = home.path().join(r"Programs\Pitboard\bin\pitboard.exe");
a_program_at(&scoop);
a_program_at(&bundled);
install(
&ctx.clone().with_schedule_program(scoop.clone()),
Permit::for_a_test(),
)
.expect("installed from Scoop's pitboard");
let the_app = home.read(&[]).with_schedule_program(bundled.clone());
assert!(
!repair(&the_app, Permit::for_a_test()).expect("nothing to do"),
"a Pitboard that is there"
);
std::fs::remove_file(&scoop).expect("Scoop's pitboard uninstalled");
let kept = path(&ctx).expect("the task's file");
let written = std::fs::read_to_string(&kept).expect("the task");
for (setting, changed) in [
("<StartWhenAvailable>true<", "<StartWhenAvailable>false<"),
(
"<StopIfGoingOnBatteries>false<",
"<StopIfGoingOnBatteries>true<",
),
("<Hidden>", "<Enabled>false</Enabled><Hidden>"),
] {
std::fs::write(&kept, written.replace(setting, changed)).expect("changed by hand");
assert!(
!repair(&the_app, Permit::for_a_test()).expect("nothing to do"),
"{changed}"
);
assert_eq!(installed_program(&ctx), Some(scoop.clone()), "{changed}");
}
std::fs::write(&kept, &written).expect("as Pitboard wrote it");
assert!(repair(&the_app, Permit::for_a_test()).expect("repaired"));
assert_eq!(installed_program(&ctx), Some(bundled));
assert_eq!(
given(&ctx),
pairs(&[("HTTPS_PROXY", "http://proxy.example.com:3128")]),
"the proxy it was given, not the app's none"
);
assert!(
!repair(&the_app, Permit::for_a_test()).expect("nothing to do"),
"the app's own, which is there"
);
}
#[test]
fn a_schedule_pitboard_did_not_write_names_no_program() {
let home = Scratch::new();
let ctx = home.ctx();
let installed = path(&ctx).expect("a scheduler here");
std::fs::create_dir_all(installed.parent().expect("its directory")).expect("made");
std::fs::write(&installed, "not what install writes\n").expect("written");
if let Some(beside) = installed.parent() {
std::fs::write(
beside.join("pitboard-renew.service"),
"[Service]\nExecStart=/bin/true\n",
)
.expect("written");
}
assert!(matches!(status(&ctx), Installed::Yes { .. }));
assert_eq!(installed_program(&ctx), None);
}
#[test]
fn the_schedule_belongs_to_the_default_home_alone() {
let home = std::env::temp_dir().join("pitboard-schedule-home");
let ctx = Context::new(home.clone());
let default = crate::host::OS.default_pitboard_home(&home);
assert!(serves(&ctx));
let trailing = PathBuf::from(format!(
"{}{}",
default.display(),
std::path::MAIN_SEPARATOR
));
assert!(serves(&ctx.clone().with_pitboard_home(trailing)));
let upper = PathBuf::from(default.to_string_lossy().to_uppercase());
assert_eq!(
serves(&ctx.clone().with_pitboard_home(upper)),
crate::host::OS == Os::Windows,
"in another case"
);
assert!(!serves(&ctx.with_pitboard_home(home.join("elsewhere"))));
}
#[test]
fn a_windows_schedule_renews_the_folder_in_userprofile_whatever_home_says() {
let profile = std::env::temp_dir().join("pitboard-profile");
let named = profile.to_string_lossy().into_owned();
let env = crate::context::Environment::on(
Os::Windows,
[
("USERPROFILE", named.as_str()),
("HOME", "/c/Users/elsewhere"),
],
);
let ctx = Context::read(&env, "cli");
let folder = profile.join("AppData").join("Local").join("Pitboard");
assert_eq!(ctx.pitboard_home, folder);
assert_eq!(env.pitboard_home(), folder, "what an app asks for");
assert!(serves(&ctx));
let run = for_its_run(
&ctx.clone()
.with_pitboard_home(profile.join("elsewhere"))
.started_by_the_schedule(),
);
assert_eq!(run.pitboard_home, folder);
}
#[test]
#[should_panic(expected = "with this account's own home")]
fn a_build_for_tests_never_runs_as_the_schedule_with_the_accounts_own_home() {
let own = {
let _real = crate::host::user::testing::reaching_the_real_home();
crate::host::user::home().expect("this account has a home")
};
let _ = for_its_run(&Context::new(own).started_by_the_schedule());
}
#[test]
fn a_schedule_is_installed_only_where_pitboard_home_is_the_default_home() {
let home = Scratch::new();
std::fs::create_dir_all(home.ctx().default_pitboard_home()).expect("a Pitboard home");
let program = home.path().join("bin/pitboard");
a_program_at(&program);
let ctx = home.ctx().with_schedule_program(program);
let default = ctx.default_pitboard_home();
let elsewhere = home.path().join("elsewhere");
let another = ctx.clone().with_pitboard_home(elsewhere.clone());
home.1.refuse_next_start();
let refused = install(&another, Permit::for_a_test()).expect_err("another home");
assert_eq!(refused.code(), "schedule_not_default_home");
assert_eq!(refused.exit_code(), 1);
assert_eq!(
refused.to_string(),
format!(
"the renewal schedule renews only the parked logins in {}, not those in {}, \
which PITBOARD_HOME names. Nothing was scheduled. To schedule it for {}, \
unset PITBOARD_HOME and install it again.",
default.display(),
elsewhere.display(),
default.display()
)
);
assert_eq!(status(&ctx), Installed::No, "nothing was written");
assert!(crate::audit::read(&another, 10).is_empty(), "nor logged");
let spelled = PathBuf::from(format!("{}/", default.display()));
let refused = install(
&ctx.clone().with_pitboard_home(spelled),
Permit::for_a_test(),
)
.expect_err("the default home reaches the scheduler, which refuses this once");
assert_eq!(refused.code(), "schedule_refused");
install(&ctx, Permit::for_a_test()).expect("installed from the default home");
assert!(matches!(status(&ctx), Installed::Yes { .. }));
assert!(!repair(&another, Permit::for_a_test()).expect("not refused"));
assert!(uninstall(&another, Permit::for_a_test()).expect("taken away from another home"));
assert_eq!(status(&ctx), Installed::No);
}
#[test]
fn a_schedule_is_given_the_proxy_variables_of_whoever_installs_it() {
let home = Scratch::new();
std::fs::create_dir_all(home.ctx().default_pitboard_home()).expect("a Pitboard home");
let program = home.path().join("bin/pitboard");
a_program_at(&program);
let proxied = home
.read(&[
("PITBOARD_NO_ARGV", "1"),
("no_proxy", "localhost"),
("HTTPS_PROXY", "http://proxy.example.com:3128"),
("ALL_PROXY", "socks5h://alice:s3cret@127.0.0.1:1080"),
("NO_PROXY", ""),
("PATH", "/usr/bin"),
])
.with_schedule_program(program.clone());
install(&proxied, Permit::for_a_test()).expect("installed");
let (all, https) = (
("ALL_PROXY", "socks5h://alice:s3cret@127.0.0.1:1080"),
("HTTPS_PROXY", "http://proxy.example.com:3128"),
);
let (handed, written, private) = match crate::host::OS {
Os::MacOs | Os::Linux => (
vec![
("PITBOARD_NO_ARGV", "1"),
all,
https,
("NO_PROXY", ""),
("no_proxy", "localhost"),
],
path(&proxied).expect("a scheduler here"),
"mode 600",
),
Os::Windows => (
vec![all, https, ("no_proxy", "localhost")],
crate::host::task::environment_file(&proxied),
"open to you, SYSTEM and Administrators",
),
};
assert_eq!(given(&proxied), pairs(&handed));
let its = installed_proxies(&proxied).expect("installed");
assert!(its.same_way(&proxied.proxy), "{its:?}");
assert_eq!(its.variables(), proxied.proxy.variables());
assert_eq!(
crate::host::fs::access(&written).map(|access| access.described),
Some(private.to_string()),
"a proxy's password is the person's alone to read"
);
let direct = home.read(&[]).with_schedule_program(program);
install(&direct, Permit::for_a_test()).expect("installed again");
assert_eq!(given(&direct), Vec::new(), "a reinstall takes this run's");
assert!(
installed_proxies(&direct)
.expect("installed")
.same_way(&direct.proxy)
);
}
#[test]
fn a_repair_writes_the_proxy_the_schedules_file_gives_and_never_the_apps() {
let home = Scratch::new();
std::fs::create_dir_all(home.ctx().default_pitboard_home()).expect("a Pitboard home");
let app = home
.path()
.join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
let bundled = home
.path()
.join("Applications/Pitboard.app/Contents/Helpers/pitboard");
a_program_at(&app);
a_program_at(&bundled);
let apps_own = [("HTTPS_PROXY", "http://app.example.com:3128")];
install(
&home.read(&[]).with_schedule_program(app.clone()),
Permit::for_a_test(),
)
.expect("0.3.0's schedule, which gives no proxy");
let the_app = home.read(&apps_own).with_schedule_program(bundled.clone());
assert!(repair(&the_app, Permit::for_a_test()).expect("repaired"));
assert_eq!(installed_program(&the_app), Some(bundled.clone()));
assert_eq!(given(&the_app), Vec::new(), "no proxy, not the app's");
let edited = pairs(&[
("https_proxy", "http://alice:s3cret@proxy.example.com:3128"),
("NO_PROXY", ".anthropic.com"),
]);
install(
&home
.read(&[
("https_proxy", "http://alice:s3cret@proxy.example.com:3128"),
("NO_PROXY", ".anthropic.com"),
])
.with_schedule_program(app),
Permit::for_a_test(),
)
.expect("an app's schedule that gives a proxy, as one edited by hand");
assert!(repair(&the_app, Permit::for_a_test()).expect("repaired"));
assert_eq!(installed_program(&the_app), Some(bundled));
assert_eq!(given(&the_app), edited, "the proxy its file gave");
}
#[test]
fn a_test_never_reaches_the_systems_own_scheduler() {
let root = std::env::temp_dir().join(format!(
"pitboard-schedule-real-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&root);
let program = root.join("bin/pitboard");
a_program_at(&program);
let ctx = Context::new(root.clone()).with_schedule_program(program);
let refused =
install(&ctx, Permit::for_a_test()).expect_err("refused before it reaches the system");
assert_eq!(refused.code(), "schedule_refused");
assert_eq!(status(&ctx), Installed::No, "and nothing is left written");
let _ = std::fs::remove_dir_all(&root);
}
}