1use crate::context::Context;
24use crate::error::{Error, Result};
25use crate::host::Scheduler;
26use crate::service::Permit;
27use std::path::{Path, PathBuf};
28
29pub(crate) const EVERY_SECONDS: u32 = 86_400;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum Installed {
37 Yes {
39 path: PathBuf,
40 every_seconds: u32,
41 },
42 No,
43 Unsupported,
45}
46
47fn scheduler(ctx: &Context) -> Option<&dyn Scheduler> {
48 ctx.host().scheduler()
49}
50
51pub fn path(ctx: &Context) -> Option<PathBuf> {
53 scheduler(ctx).map(|s| s.location(ctx))
54}
55
56pub fn status(ctx: &Context) -> Installed {
57 match scheduler(ctx) {
58 None => Installed::Unsupported,
59 Some(s) if s.installed(ctx) => Installed::Yes {
60 path: s.location(ctx),
61 every_seconds: EVERY_SECONDS,
62 },
63 Some(_) => Installed::No,
64 }
65}
66
67pub const SCHEDULED_RUN: [&str; 2] = ["renew", "--scheduled"];
75
76pub(crate) fn serves(ctx: &Context) -> bool {
87 crate::home::dir(ctx) == crate::host::default_pitboard_home(ctx.home())
88}
89
90pub(crate) fn started_this_run(ctx: &Context) -> bool {
94 ctx.marked_as_scheduled()
95 || scheduler(ctx).is_some_and(|s| s.started_this_process(ctx.scheduled_job()))
96}
97
98pub(crate) fn for_its_run(ctx: &Context) -> Context {
112 if !started_this_run(ctx) {
113 return ctx.clone();
114 }
115 #[cfg(any(test, feature = "test-support"))]
116 assert!(
117 !crate::host::user::is_the_accounts_own_home(ctx.home()),
118 "a build for tests ran as the daily renewal schedule with this account's own home, \
119 where it would renew the real Pitboard directory: give it a HOME of its own"
120 );
121 ctx.clone()
122 .with_pitboard_home(crate::host::default_pitboard_home(ctx.home()))
123}
124
125fn program(ctx: &Context) -> Result<PathBuf> {
128 if let Some(program) = ctx.schedule_program() {
129 return lasting(program).map(Path::to_path_buf);
130 }
131 crate::host::current_program().map_err(|source| Error::HomeUnwritable {
132 path: PathBuf::from("the running Pitboard"),
133 source,
134 })
135}
136
137fn lasting(program: &Path) -> Result<&Path> {
144 if in_a_temporary_copy(program) {
145 return Err(Error::ScheduleProgramTemporary {
146 path: program.to_path_buf(),
147 });
148 }
149 if !program.is_file() {
150 return Err(Error::ScheduleProgramMissing {
151 path: program.to_path_buf(),
152 });
153 }
154 Ok(program)
155}
156
157pub fn in_a_temporary_copy(program: &Path) -> bool {
162 program
163 .components()
164 .any(|part| part.as_os_str() == "AppTranslocation")
165}
166
167pub fn installed_program(ctx: &Context) -> Option<PathBuf> {
171 scheduler(ctx)?.program(ctx)
172}
173
174fn environment(
180 ctx: &Context,
181 proxy: impl IntoIterator<Item = (String, String)>,
182) -> Vec<(String, String)> {
183 let refusing = (!ctx.argv_fallback()).then(|| ("PITBOARD_NO_ARGV".into(), "1".into()));
184 refusing.into_iter().chain(proxy).collect()
185}
186
187fn installed_proxy_variables(ctx: &Context) -> Vec<(String, String)> {
190 let Some(scheduler) = scheduler(ctx) else {
191 return Vec::new();
192 };
193 scheduler
194 .environment(ctx)
195 .into_iter()
196 .filter(|(name, _)| {
197 crate::proxy::NAMING
198 .iter()
199 .chain(&crate::proxy::EXEMPTING)
200 .any(|proxy| proxy == name)
201 })
202 .collect()
203}
204
205pub(crate) fn installed_proxies(ctx: &Context) -> Option<crate::proxy::Proxies> {
208 if !scheduler(ctx)?.installed(ctx) {
209 return None;
210 }
211 let given: crate::context::Environment = installed_proxy_variables(ctx).into_iter().collect();
212 Some(crate::proxy::Proxies::read(&given))
213}
214
215pub fn install(ctx: &Context, permit: Permit) -> Result<PathBuf> {
226 if !serves(ctx) {
227 return Err(Error::ScheduleNotDefaultHome {
228 home: crate::home::dir(ctx),
229 default: crate::host::default_pitboard_home(ctx.home()),
230 });
231 }
232 let proxy = ctx
233 .proxy
234 .variables()
235 .iter()
236 .map(|(name, value)| ((*name).to_string(), value.clone()));
237 let path = put(ctx, permit, &program(ctx)?, &environment(ctx, proxy))?;
238 crate::audit::record(ctx, permit, "schedule", "install", "ok");
239 Ok(path)
240}
241
242pub fn repair(ctx: &Context, permit: Permit) -> Result<bool> {
260 if scheduler(ctx).is_none() || started_this_run(ctx) {
261 return Ok(false);
262 }
263 let Some(named) = ctx.schedule_program() else {
264 return Ok(false);
265 };
266 if !serves(ctx)
267 || lasting(named).is_err()
268 || !installed_program(ctx).is_some_and(|program| an_apps_own_program(&program))
269 {
270 return Ok(false);
271 }
272 let kept = installed_proxy_variables(ctx);
273 let repaired = put(ctx, permit, named, &environment(ctx, kept));
274 crate::audit::record(
275 ctx,
276 permit,
277 "schedule",
278 "repair",
279 match &repaired {
280 Ok(_) => "ok",
281 Err(e) => e.code(),
282 },
283 );
284 repaired.map(|_| true)
285}
286
287pub(crate) fn an_apps_own_program(program: &Path) -> bool {
290 let mut dirs = program
291 .ancestors()
292 .skip(1)
293 .map(|dir| dir.file_name().and_then(|n| n.to_str()));
294 dirs.next() == Some(Some("MacOS")) && dirs.next() == Some(Some("Contents"))
295}
296
297fn put(
299 ctx: &Context,
300 permit: Permit,
301 program: &Path,
302 environment: &[(String, String)],
303) -> Result<PathBuf> {
304 let Some(scheduler) = scheduler(ctx) else {
305 return Err(Error::ScheduleUnsupported);
306 };
307 scheduler.put(ctx, permit, program, environment)?;
308 Ok(scheduler.location(ctx))
309}
310
311pub fn uninstall(ctx: &Context, permit: Permit) -> Result<bool> {
313 let Some(scheduler) = scheduler(ctx) else {
314 return Err(Error::ScheduleUnsupported);
315 };
316 if !scheduler.remove(ctx, permit)? {
317 return Ok(false);
318 }
319 crate::audit::record(ctx, permit, "schedule", "uninstall", "ok");
320 Ok(true)
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::host::memory::MemoryHost;
327 use std::sync::Arc;
328
329 struct Scratch(PathBuf, Arc<MemoryHost>);
332
333 impl Scratch {
334 fn new(name: &str) -> Scratch {
335 let root = std::env::temp_dir().join(format!(
336 "pitboard-schedule-{name}-{}-{:?}",
337 std::process::id(),
338 std::thread::current().id()
339 ));
340 let _ = std::fs::remove_dir_all(&root);
341 std::fs::create_dir_all(&root).expect("a scratch home");
342 Scratch(root, MemoryHost::new())
343 }
344
345 fn ctx(&self) -> Context {
346 Context::new(self.0.clone()).with_memory_stores(Arc::clone(&self.1))
347 }
348
349 fn read(&self, vars: &[(&str, &str)]) -> Context {
352 let home = self.0.to_string_lossy().into_owned();
353 let env: crate::context::Environment = [("HOME", home.as_str())]
354 .iter()
355 .chain(vars)
356 .copied()
357 .collect();
358 Context::for_command_line(&env).with_memory_stores(Arc::clone(&self.1))
359 }
360 }
361
362 fn given(ctx: &Context) -> Vec<(String, String)> {
364 scheduler(ctx).expect("a scheduler").environment(ctx)
365 }
366
367 fn pairs(given: &[(&str, &str)]) -> Vec<(String, String)> {
368 given
369 .iter()
370 .map(|(name, value)| ((*name).to_string(), (*value).to_string()))
371 .collect()
372 }
373
374 impl Drop for Scratch {
375 fn drop(&mut self) {
376 let _ = std::fs::remove_dir_all(&self.0);
377 }
378 }
379
380 fn a_program_at(path: &Path) {
382 std::fs::create_dir_all(path.parent().expect("a directory")).expect("its directory");
383 std::fs::write(path, "").expect("a program");
384 }
385
386 #[test]
388 fn a_schedule_the_scheduler_will_not_start_is_not_left_on_disk() {
389 let home = Scratch::new("refused");
390 let program = home.0.join("bin/pitboard");
391 a_program_at(&program);
392 let ctx = home.ctx().with_schedule_program(program);
393
394 home.1.refuse_next_start();
395 let refused = install(&ctx, Permit::for_a_test()).expect_err("refused");
396
397 assert_eq!(refused.code(), "schedule_refused");
398 assert_eq!(status(&ctx), Installed::No);
399 assert_eq!(installed_program(&ctx), None);
400 }
401
402 #[test]
405 fn a_repair_the_scheduler_will_not_start_leaves_the_schedule_as_it_was() {
406 let home = Scratch::new("repair-refused");
407 std::fs::create_dir_all(home.0.join(".pitboard")).expect("a Pitboard home");
408 let app = home
409 .0
410 .join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
411 let bundled = home
412 .0
413 .join("Applications/Pitboard.app/Contents/Helpers/pitboard");
414 a_program_at(&app);
415 a_program_at(&bundled);
416 let ctx = home.ctx();
417 install(
418 &ctx.clone().with_schedule_program(app.clone()),
419 Permit::for_a_test(),
420 )
421 .expect("0.3.0's schedule");
422
423 home.1.refuse_next_start();
424 let refused = repair(
425 &ctx.clone().with_schedule_program(bundled),
426 Permit::for_a_test(),
427 )
428 .expect_err("refused");
429
430 assert_eq!(refused.code(), "schedule_refused");
431 assert!(matches!(status(&ctx), Installed::Yes { .. }));
432 assert_eq!(installed_program(&ctx), Some(app));
433 }
434
435 #[test]
436 fn nothing_is_installed_on_a_machine_where_nothing_was_installed() {
437 let home = Scratch::new("none");
438 let ctx = home.ctx();
439 assert_eq!(status(&ctx), Installed::No);
440 assert_eq!(installed_program(&ctx), None);
441 }
442
443 #[test]
445 fn a_machine_with_no_scheduler_says_so() {
446 let home = Scratch::new("unsupported");
447 home.1.without_a_scheduler();
448 let program = home.0.join("bin/pitboard");
449 a_program_at(&program);
450 let ctx = home.ctx().with_schedule_program(program);
451 assert_eq!(status(&ctx), Installed::Unsupported);
452 assert_eq!(path(&ctx), None);
453 assert_eq!(
454 install(&ctx, Permit::for_a_test())
455 .expect_err("nowhere")
456 .code(),
457 "schedule_unsupported"
458 );
459 assert_eq!(
460 uninstall(&ctx, Permit::for_a_test())
461 .expect_err("nowhere")
462 .code(),
463 "schedule_unsupported"
464 );
465 assert!(!repair(&ctx, Permit::for_a_test()).expect("nothing to do"));
466 }
467
468 #[test]
469 fn the_schedule_runs_the_pitboard_the_context_names_and_otherwise_this_one() {
470 let home = Scratch::new("program");
471 let ctx = home.ctx();
472 let scheduled = program(&ctx).expect("this program");
473 let running = std::env::current_exe().expect("this test's own program");
474 assert_eq!(
475 std::fs::canonicalize(&scheduled).expect("there"),
476 std::fs::canonicalize(&running).expect("there"),
477 "the command line schedules itself"
478 );
479 let bundled = home
480 .0
481 .join("Applications/Pitboard.app/Contents/Helpers/pitboard");
482 a_program_at(&bundled);
483 assert_eq!(
484 program(&ctx.with_schedule_program(bundled.clone())).expect("the named one"),
485 bundled,
486 "an app schedules the command line it comes with"
487 );
488 }
489
490 #[test]
495 fn a_named_pitboard_that_will_not_be_there_is_refused() {
496 let home = Scratch::new("refused");
497 let ctx = home.ctx();
498
499 let missing = home.0.join("Pitboard.app/Contents/Helpers/pitboard");
500 let refused = install(
501 &ctx.clone().with_schedule_program(missing.clone()),
502 Permit::for_a_test(),
503 )
504 .expect_err("nothing there to run");
505 assert_eq!(refused.code(), "schedule_program_missing");
506 assert!(
507 refused.to_string().contains(&missing.display().to_string()),
508 "{refused}"
509 );
510
511 let temporary = home
512 .0
513 .join("AppTranslocation/6A1C/d/Pitboard.app/Contents/Helpers/pitboard");
514 a_program_at(&temporary);
515 let refused = install(
516 &ctx.clone().with_schedule_program(temporary.clone()),
517 Permit::for_a_test(),
518 )
519 .expect_err("a copy that goes away");
520 assert_eq!(refused.code(), "schedule_program_temporary");
521 assert!(
522 refused
523 .to_string()
524 .contains(&temporary.display().to_string())
525 && refused.to_string().contains("Applications folder"),
526 "{refused}"
527 );
528
529 assert_eq!(status(&ctx), Installed::No, "nothing was written");
530 }
531
532 #[test]
536 fn a_temporary_copy_is_told_by_its_path() {
537 for (path, temporary) in [
538 (
539 "/private/var/folders/xy/abc/T/AppTranslocation/0A1B2C/d/Pitboard.app/Contents/\
540 Helpers/pitboard",
541 true,
542 ),
543 (
544 "/Applications/Pitboard.app/Contents/Helpers/pitboard",
545 false,
546 ),
547 (
548 "/Users/x/MyAppTranslocation/Pitboard.app/Contents/Helpers/pitboard",
549 false,
550 ),
551 (
552 "/Users/x/AppTranslocations/Pitboard.app/Contents/Helpers/pitboard",
553 false,
554 ),
555 ] {
556 assert_eq!(in_a_temporary_copy(Path::new(path)), temporary, "{path}");
557 }
558 }
559
560 #[test]
563 fn an_installed_schedule_says_which_pitboard_it_runs() {
564 let home = Scratch::new("installed");
565 let bundled = home
566 .0
567 .join("Tools&Apps/Pitboard.app/Contents/Helpers/pitboard");
568 a_program_at(&bundled);
569 let ctx = home.ctx().with_schedule_program(bundled.clone());
570
571 install(&ctx, Permit::for_a_test()).expect("installed");
572 assert!(matches!(status(&ctx), Installed::Yes { .. }));
573 assert_eq!(installed_program(&ctx), Some(bundled));
574
575 assert!(uninstall(&ctx, Permit::for_a_test()).expect("taken away"));
576 assert_eq!(status(&ctx), Installed::No);
577 assert_eq!(installed_program(&ctx), None);
578 assert!(!uninstall(&ctx, Permit::for_a_test()).expect("nothing there"));
579 }
580
581 #[test]
585 fn a_schedule_that_runs_an_app_is_pointed_at_its_command_line() {
586 let home = Scratch::new("repair");
587 std::fs::create_dir_all(home.0.join(".pitboard")).expect("a Pitboard home");
588 let app = home
589 .0
590 .join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
591 let bundled = home
592 .0
593 .join("Applications/Pitboard.app/Contents/Helpers/pitboard");
594 a_program_at(&app);
595 a_program_at(&bundled);
596 let ctx = home.ctx();
597 let the_app = ctx.clone().with_schedule_program(bundled.clone());
598
599 assert!(
600 !repair(&the_app, Permit::for_a_test()).expect("nothing to do"),
601 "nothing installed"
602 );
603
604 install(
605 &ctx.clone().with_schedule_program(app.clone()),
606 Permit::for_a_test(),
607 )
608 .expect("0.3.0's schedule");
609 assert!(
610 !repair(&ctx, Permit::for_a_test()).expect("nothing to do"),
611 "no command line named"
612 );
613 let gone = home.0.join("Old.app/Contents/Helpers/pitboard");
614 assert!(
615 !repair(
616 &ctx.clone().with_schedule_program(gone),
617 Permit::for_a_test()
618 )
619 .expect("nothing to do"),
620 "a command line that is not there"
621 );
622 assert!(
623 !repair(
624 &the_app.clone().with_pitboard_home(home.0.join("elsewhere")),
625 Permit::for_a_test()
626 )
627 .expect("nothing to do"),
628 "a schedule another home's Pitboard looks after"
629 );
630 assert_eq!(installed_program(&ctx), Some(app.clone()));
631
632 assert!(repair(&the_app, Permit::for_a_test()).expect("repaired"));
633 assert_eq!(installed_program(&ctx), Some(bundled));
634 let logged = crate::audit::read(&ctx, 1);
635 assert_eq!(
636 logged
637 .iter()
638 .map(|e| (e.verb.as_str(), e.subject.as_str(), e.outcome.as_str()))
639 .collect::<Vec<_>>(),
640 [("schedule", "repair", "ok")]
641 );
642
643 assert!(
644 !repair(&the_app, Permit::for_a_test()).expect("nothing to do"),
645 "a schedule that runs a command line already"
646 );
647 install(
648 &ctx.clone().with_schedule_program(app.clone()),
649 Permit::for_a_test(),
650 )
651 .expect("the app again");
652 let temporary = home
653 .0
654 .join("AppTranslocation/6A1C/d/Pitboard.app/Contents/Helpers/pitboard");
655 a_program_at(&temporary);
656 assert!(
657 !repair(
658 &ctx.clone().with_schedule_program(temporary),
659 Permit::for_a_test()
660 )
661 .expect("nothing to do"),
662 "a command line that is gone once the app quits"
663 );
664 assert_eq!(installed_program(&ctx), Some(app));
665 }
666
667 #[test]
670 fn a_process_another_job_started_still_repairs() {
671 let home = Scratch::new("another-job");
672 std::fs::create_dir_all(home.0.join(".pitboard")).expect("a Pitboard home");
673 let app = home
674 .0
675 .join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
676 let bundled = home
677 .0
678 .join("Applications/Pitboard.app/Contents/Helpers/pitboard");
679 a_program_at(&app);
680 a_program_at(&bundled);
681 let ctx = home.ctx();
682 install(
683 &ctx.clone().with_schedule_program(app),
684 Permit::for_a_test(),
685 )
686 .expect("0.3.0's schedule");
687
688 let opened = ctx
689 .clone()
690 .with_schedule_program(bundled.clone())
691 .with_scheduled_job("application.com.datlechin.pitboard.1.2".into());
692 assert!(repair(&opened, Permit::for_a_test()).expect("repaired"));
693 assert_eq!(installed_program(&ctx), Some(bundled));
694 }
695
696 #[test]
698 fn a_schedule_pitboard_did_not_write_names_no_program() {
699 let home = Scratch::new("foreign");
700 let ctx = home.ctx();
701 let installed = path(&ctx).expect("a scheduler here");
702 std::fs::create_dir_all(installed.parent().expect("its directory")).expect("made");
703 std::fs::write(&installed, "not what install writes\n").expect("written");
704 if let Some(beside) = installed.parent() {
705 std::fs::write(
707 beside.join("pitboard-renew.service"),
708 "[Service]\nExecStart=/bin/true\n",
709 )
710 .expect("written");
711 }
712
713 assert!(matches!(status(&ctx), Installed::Yes { .. }));
714 assert_eq!(installed_program(&ctx), None);
715 }
716
717 #[test]
720 fn the_schedule_belongs_to_the_default_home_alone() {
721 let ctx = Context::new(PathBuf::from("/home/x"));
722 assert!(serves(&ctx));
723 assert!(serves(
724 &ctx.clone()
725 .with_pitboard_home(PathBuf::from("/home/x/.pitboard/"))
726 ));
727 assert!(!serves(
728 &ctx.with_pitboard_home(PathBuf::from("/tmp/elsewhere"))
729 ));
730 }
731
732 #[test]
739 fn a_schedule_is_installed_only_where_pitboard_home_is_the_default_home() {
740 let home = Scratch::new("default-home");
741 std::fs::create_dir_all(home.0.join(".pitboard")).expect("a Pitboard home");
742 let program = home.0.join("bin/pitboard");
743 a_program_at(&program);
744 let ctx = home.ctx().with_schedule_program(program);
745 let default = home.0.join(".pitboard");
746 let elsewhere = home.0.join("elsewhere");
747 let another = ctx.clone().with_pitboard_home(elsewhere.clone());
748
749 home.1.refuse_next_start();
750 let refused = install(&another, Permit::for_a_test()).expect_err("another home");
751 assert_eq!(refused.code(), "schedule_not_default_home");
752 assert_eq!(refused.exit_code(), 1);
753 assert_eq!(
754 refused.to_string(),
755 format!(
756 "the renewal schedule renews only the parked logins in {}, not those in {}, \
757 which PITBOARD_HOME names. Nothing was scheduled. To schedule it for {}, \
758 unset PITBOARD_HOME and install it again.",
759 default.display(),
760 elsewhere.display(),
761 default.display()
762 )
763 );
764 assert_eq!(status(&ctx), Installed::No, "nothing was written");
765 assert!(crate::audit::read(&another, 10).is_empty(), "nor logged");
766
767 let spelled = PathBuf::from(format!("{}/", default.display()));
768 let refused = install(
769 &ctx.clone().with_pitboard_home(spelled),
770 Permit::for_a_test(),
771 )
772 .expect_err("the default home reaches the scheduler, which refuses this once");
773 assert_eq!(refused.code(), "schedule_refused");
774 install(&ctx, Permit::for_a_test()).expect("installed from the default home");
775 assert!(matches!(status(&ctx), Installed::Yes { .. }));
776
777 assert!(!repair(&another, Permit::for_a_test()).expect("not refused"));
778 assert!(uninstall(&another, Permit::for_a_test()).expect("taken away from another home"));
779 assert_eq!(status(&ctx), Installed::No);
780 }
781
782 #[test]
788 fn a_schedule_is_given_the_proxy_variables_of_whoever_installs_it() {
789 let home = Scratch::new("proxy-install");
790 std::fs::create_dir_all(home.0.join(".pitboard")).expect("a Pitboard home");
791 let program = home.0.join("bin/pitboard");
792 a_program_at(&program);
793 let proxied = home
794 .read(&[
795 ("PITBOARD_NO_ARGV", "1"),
796 ("no_proxy", "localhost"),
797 ("HTTPS_PROXY", "http://proxy.example.com:3128"),
798 ("ALL_PROXY", "socks5h://alice:s3cret@127.0.0.1:1080"),
799 ("NO_PROXY", ""),
800 ("PATH", "/usr/bin"),
801 ])
802 .with_schedule_program(program.clone());
803
804 install(&proxied, Permit::for_a_test()).expect("installed");
805 assert_eq!(
806 given(&proxied),
807 pairs(&[
808 ("PITBOARD_NO_ARGV", "1"),
809 ("ALL_PROXY", "socks5h://alice:s3cret@127.0.0.1:1080"),
810 ("HTTPS_PROXY", "http://proxy.example.com:3128"),
811 ("NO_PROXY", ""),
812 ("no_proxy", "localhost"),
813 ])
814 );
815 let its = installed_proxies(&proxied).expect("installed");
816 assert!(its.same_way(&proxied.proxy), "{its:?}");
817 assert_eq!(its.variables(), proxied.proxy.variables());
818 let written = path(&proxied).expect("a scheduler here");
819 assert_eq!(
820 crate::host::fs::access(&written).map(|access| access.described),
821 Some("mode 600".to_string()),
822 "a proxy's password is the person's alone to read"
823 );
824
825 let direct = home.read(&[]).with_schedule_program(program);
826 install(&direct, Permit::for_a_test()).expect("installed again");
827 assert_eq!(given(&direct), Vec::new(), "a reinstall takes this run's");
828 assert!(
829 installed_proxies(&direct)
830 .expect("installed")
831 .same_way(&direct.proxy)
832 );
833 }
834
835 #[test]
841 fn a_repair_writes_the_proxy_the_schedules_file_gives_and_never_the_apps() {
842 let home = Scratch::new("proxy-repair");
843 std::fs::create_dir_all(home.0.join(".pitboard")).expect("a Pitboard home");
844 let app = home
845 .0
846 .join("Applications/Pitboard.app/Contents/MacOS/Pitboard");
847 let bundled = home
848 .0
849 .join("Applications/Pitboard.app/Contents/Helpers/pitboard");
850 a_program_at(&app);
851 a_program_at(&bundled);
852 let apps_own = [("HTTPS_PROXY", "http://app.example.com:3128")];
853
854 install(
855 &home.read(&[]).with_schedule_program(app.clone()),
856 Permit::for_a_test(),
857 )
858 .expect("0.3.0's schedule, which gives no proxy");
859 let the_app = home.read(&apps_own).with_schedule_program(bundled.clone());
860 assert!(repair(&the_app, Permit::for_a_test()).expect("repaired"));
861 assert_eq!(installed_program(&the_app), Some(bundled.clone()));
862 assert_eq!(given(&the_app), Vec::new(), "no proxy, not the app's");
863
864 let edited = pairs(&[
865 ("https_proxy", "http://alice:s3cret@proxy.example.com:3128"),
866 ("NO_PROXY", ".anthropic.com"),
867 ]);
868 install(
869 &home
870 .read(&[
871 ("https_proxy", "http://alice:s3cret@proxy.example.com:3128"),
872 ("NO_PROXY", ".anthropic.com"),
873 ])
874 .with_schedule_program(app),
875 Permit::for_a_test(),
876 )
877 .expect("an app's schedule that gives a proxy, as one edited by hand");
878 assert!(repair(&the_app, Permit::for_a_test()).expect("repaired"));
879 assert_eq!(installed_program(&the_app), Some(bundled));
880 assert_eq!(given(&the_app), edited, "the proxy its file gave");
881 }
882
883 #[test]
886 fn a_test_never_reaches_the_systems_own_scheduler() {
887 let root = std::env::temp_dir().join(format!(
888 "pitboard-schedule-real-{}-{:?}",
889 std::process::id(),
890 std::thread::current().id()
891 ));
892 let _ = std::fs::remove_dir_all(&root);
893 let program = root.join("bin/pitboard");
894 a_program_at(&program);
895 let ctx = Context::new(root.clone()).with_schedule_program(program);
896 let refused =
897 install(&ctx, Permit::for_a_test()).expect_err("refused before it reaches the system");
898 assert_eq!(refused.code(), "schedule_refused");
899 assert_eq!(status(&ctx), Installed::No, "and nothing is left written");
900 let _ = std::fs::remove_dir_all(&root);
901 }
902}