922 lines
30 KiB
Rust
922 lines
30 KiB
Rust
use super::*;
|
|
use crate::component_cutover::{
|
|
CutoverOperation, EffectDisposition, LegacyServiceState, MutationDirection, MutationEffect,
|
|
MutationRecord, StateFingerprint,
|
|
};
|
|
use crate::process::{
|
|
FullServiceSnapshot, ServiceBaseConfigSnapshot, ServiceSecuritySnapshot, ServiceStableState,
|
|
SERVICE_CONFIG2_KINDS,
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
enum Call {
|
|
CaptureLegacy,
|
|
QueryLegacy,
|
|
QueryCurrent,
|
|
QueryComplete,
|
|
QueryLegacyPolicy(ServiceConfig2Kind),
|
|
QueryCurrentPolicy(ServiceConfig2Kind),
|
|
QueryCurrentSecurity,
|
|
StopLegacy,
|
|
DeleteLegacy,
|
|
CreateCurrent,
|
|
SetCurrentPolicy(ServiceConfig2Kind),
|
|
SetCurrentSecurity,
|
|
StartCurrent,
|
|
StopCurrent,
|
|
DeleteCurrent,
|
|
CreateLegacy,
|
|
RestoreLegacyPolicy(ServiceConfig2Kind),
|
|
RestoreLegacySecurity,
|
|
StartLegacy,
|
|
}
|
|
|
|
struct FakeScm {
|
|
calls: Vec<Call>,
|
|
fail_on: Option<Call>,
|
|
before: ServiceRestoreSnapshot,
|
|
complete: CompleteServiceObservation,
|
|
current_base: ServiceBaseConfigSnapshot,
|
|
}
|
|
|
|
impl FakeScm {
|
|
fn new() -> Self {
|
|
let current_base = expected_current_proxifyre_service_base(
|
|
&std::env::temp_dir().join("ProxyWarden-current-ProxiFyre.exe"),
|
|
)
|
|
.expect("current base fixture");
|
|
Self {
|
|
calls: Vec::new(),
|
|
fail_on: None,
|
|
before: before_state(),
|
|
complete: CompleteServiceObservation::Missing,
|
|
current_base,
|
|
}
|
|
}
|
|
|
|
fn record(&mut self, call: Call) -> Result<(), ProxifyreNativeHostError> {
|
|
self.calls.push(call.clone());
|
|
if self.fail_on.as_ref() == Some(&call) {
|
|
Err(ProxifyreNativeHostError)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn missing_policy() -> ServicePolicySnapshot {
|
|
ServicePolicySnapshot {
|
|
service: crate::process::ServiceSnapshot {
|
|
exists: false,
|
|
state: None,
|
|
path_name: None,
|
|
process_id: None,
|
|
},
|
|
path_matches: false,
|
|
demand_start: false,
|
|
failure_recovery_disabled: false,
|
|
dacl_matches: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ProxifyreCutoverScm for FakeScm {
|
|
fn capture_legacy_service(
|
|
&mut self,
|
|
) -> Result<ServiceRestoreSnapshot, ProxifyreNativeHostError> {
|
|
self.record(Call::CaptureLegacy)?;
|
|
Ok(self.before.clone())
|
|
}
|
|
|
|
fn query_legacy_service(&mut self) -> Result<ServicePolicySnapshot, ProxifyreNativeHostError> {
|
|
self.record(Call::QueryLegacy)?;
|
|
Ok(Self::missing_policy())
|
|
}
|
|
|
|
fn query_current_service(&mut self) -> Result<ServicePolicySnapshot, ProxifyreNativeHostError> {
|
|
self.record(Call::QueryCurrent)?;
|
|
Ok(Self::missing_policy())
|
|
}
|
|
|
|
fn query_complete_service(
|
|
&mut self,
|
|
) -> Result<CompleteServiceObservation, ProxifyreNativeHostError> {
|
|
self.record(Call::QueryComplete)?;
|
|
Ok(self.complete.clone())
|
|
}
|
|
|
|
fn expected_current_service_base(
|
|
&self,
|
|
) -> Result<ServiceBaseConfigSnapshot, ProxifyreNativeHostError> {
|
|
Ok(self.current_base.clone())
|
|
}
|
|
|
|
fn query_legacy_service_policy(
|
|
&mut self,
|
|
kind: ServiceConfig2Kind,
|
|
) -> Result<Option<ServiceConfig2Snapshot>, ProxifyreNativeHostError> {
|
|
self.record(Call::QueryLegacyPolicy(kind))?;
|
|
Ok(self.before.config2(kind).cloned())
|
|
}
|
|
|
|
fn query_current_service_policy(
|
|
&mut self,
|
|
kind: ServiceConfig2Kind,
|
|
) -> Result<Option<ServiceConfig2Snapshot>, ProxifyreNativeHostError> {
|
|
self.record(Call::QueryCurrentPolicy(kind))?;
|
|
Ok(None)
|
|
}
|
|
|
|
fn current_service_security_matches(&mut self) -> Result<bool, ProxifyreNativeHostError> {
|
|
self.record(Call::QueryCurrentSecurity)?;
|
|
Ok(false)
|
|
}
|
|
|
|
fn stop_legacy_service(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::StopLegacy)
|
|
}
|
|
|
|
fn delete_legacy_service(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::DeleteLegacy)
|
|
}
|
|
|
|
fn create_current_service(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::CreateCurrent)
|
|
}
|
|
|
|
fn set_current_service_policy(
|
|
&mut self,
|
|
kind: ServiceConfig2Kind,
|
|
) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::SetCurrentPolicy(kind))
|
|
}
|
|
|
|
fn set_current_service_security(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::SetCurrentSecurity)
|
|
}
|
|
|
|
fn start_current_service(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::StartCurrent)
|
|
}
|
|
|
|
fn stop_current_service(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::StopCurrent)
|
|
}
|
|
|
|
fn delete_current_service(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::DeleteCurrent)
|
|
}
|
|
|
|
fn create_legacy_service(
|
|
&mut self,
|
|
_before: &ServiceRestoreSnapshot,
|
|
) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::CreateLegacy)
|
|
}
|
|
|
|
fn restore_legacy_service_policy(
|
|
&mut self,
|
|
snapshot: &ServiceConfig2Snapshot,
|
|
) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::RestoreLegacyPolicy(snapshot.kind()))
|
|
}
|
|
|
|
fn restore_legacy_service_security(
|
|
&mut self,
|
|
_before: &ServiceRestoreSnapshot,
|
|
) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::RestoreLegacySecurity)
|
|
}
|
|
|
|
fn start_legacy_service(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(Call::StartLegacy)
|
|
}
|
|
}
|
|
|
|
fn before_state() -> ServiceRestoreSnapshot {
|
|
FullServiceSnapshot {
|
|
service_name: PROXIFYRE_MANAGED_SERVICE_NAME.to_owned(),
|
|
base: ServiceBaseConfigSnapshot {
|
|
service_type: 0x10,
|
|
start_type: 2,
|
|
error_control: 1,
|
|
binary_path_name: concat!(
|
|
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" "#,
|
|
r#"-displayname "ProxiFyre Service" -servicename "ProxiFyreService""#
|
|
)
|
|
.to_owned(),
|
|
load_order_group: None,
|
|
tag_id: 0,
|
|
dependencies: Vec::new(),
|
|
service_start_name: "LocalSystem".to_owned(),
|
|
display_name: "ProxiFyre Service".to_owned(),
|
|
},
|
|
config2: SERVICE_CONFIG2_KINDS
|
|
.iter()
|
|
.copied()
|
|
.map(expected_current_proxifyre_service_policy)
|
|
.collect(),
|
|
security: ServiceSecuritySnapshot {
|
|
self_relative_descriptor: vec![1, 2, 3],
|
|
untrusted_mutation_rights: false,
|
|
},
|
|
original_state: ServiceStableState::Running,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_current_is_exactly_one_call_and_never_starts() {
|
|
let mut host = FakeScm::new();
|
|
let before = host.before.clone();
|
|
|
|
assert!(mutate_proxifyre_cutover_scm(
|
|
&mut host,
|
|
&CutoverOperation::CreateCurrentService,
|
|
&before,
|
|
)
|
|
.expect("SCM mutation dispatch"));
|
|
assert_eq!(host.calls, vec![Call::CreateCurrent]);
|
|
}
|
|
|
|
#[test]
|
|
fn collision_or_failure_stops_after_the_single_selected_mutation() {
|
|
let mut host = FakeScm::new();
|
|
host.fail_on = Some(Call::DeleteLegacy);
|
|
let before = host.before.clone();
|
|
|
|
mutate_proxifyre_cutover_scm(&mut host, &CutoverOperation::DeleteLegacyService, &before)
|
|
.expect_err("collision/failure must surface");
|
|
assert_eq!(host.calls, vec![Call::DeleteLegacy]);
|
|
}
|
|
|
|
#[test]
|
|
fn restore_policy_selects_only_the_requested_captured_record() {
|
|
let mut host = FakeScm::new();
|
|
let before = host.before.clone();
|
|
|
|
assert!(mutate_proxifyre_cutover_scm(
|
|
&mut host,
|
|
&CutoverOperation::RestoreLegacyServicePolicy(ServiceConfig2Kind::Triggers),
|
|
&before,
|
|
)
|
|
.expect("restore dispatch"));
|
|
assert_eq!(
|
|
host.calls,
|
|
vec![Call::RestoreLegacyPolicy(ServiceConfig2Kind::Triggers)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_only_missing_policy_is_typed_absence_and_never_mutates() {
|
|
let mut host = FakeScm::new();
|
|
assert_eq!(
|
|
host.query_current_service_policy(ServiceConfig2Kind::Description)
|
|
.expect("read-only query"),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
host.calls,
|
|
vec![Call::QueryCurrentPolicy(ServiceConfig2Kind::Description)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn non_scm_operation_is_not_claimed_or_mutated() {
|
|
let mut host = FakeScm::new();
|
|
let before = host.before.clone();
|
|
assert!(!mutate_proxifyre_cutover_scm(
|
|
&mut host,
|
|
&CutoverOperation::HardenLegacyRootSecurity,
|
|
&before,
|
|
)
|
|
.expect("non-SCM dispatch"));
|
|
assert!(host.calls.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn expected_current_policy_covers_every_config2_kind() {
|
|
for kind in SERVICE_CONFIG2_KINDS {
|
|
assert_eq!(expected_current_proxifyre_service_policy(kind).kind(), kind);
|
|
}
|
|
assert!(matches!(
|
|
expected_current_proxifyre_service_policy(ServiceConfig2Kind::Triggers),
|
|
ServiceConfig2Snapshot::Triggers(ref triggers) if triggers.is_empty()
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn scm_observer_matches_typed_expected_fingerprint_for_every_scm_operation() {
|
|
let before = before_state();
|
|
let operations = vec![
|
|
CutoverOperation::StopLegacyService,
|
|
CutoverOperation::DeleteLegacyService,
|
|
CutoverOperation::CreateCurrentService,
|
|
CutoverOperation::SetCurrentServicePolicy(ServiceConfig2Kind::Description),
|
|
CutoverOperation::SetCurrentServiceSecurity,
|
|
CutoverOperation::StartCurrentService,
|
|
CutoverOperation::StopCurrentService,
|
|
CutoverOperation::DeleteCurrentService,
|
|
CutoverOperation::CreateLegacyService,
|
|
CutoverOperation::RestoreLegacyServicePolicy(ServiceConfig2Kind::Triggers),
|
|
CutoverOperation::RestoreLegacyServiceSecurity,
|
|
CutoverOperation::StartLegacyService,
|
|
];
|
|
|
|
for operation in operations {
|
|
let mut host = FakeScm::new();
|
|
host.complete = satisfying_scm_observation(&operation, &before, &host.current_base);
|
|
assert_eq!(
|
|
observe_proxifyre_cutover_scm_state(&mut host, &operation, &before)
|
|
.expect("typed complete SCM observation"),
|
|
expected_proxifyre_cutover_scm_effect(&operation).expect("typed expected SCM effect"),
|
|
"operation {operation:?}"
|
|
);
|
|
assert_eq!(host.calls, vec![Call::QueryComplete]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scm_unexpected_fingerprint_preserves_complete_drift_instead_of_boolean_bucket() {
|
|
let before = before_state();
|
|
let operation = CutoverOperation::CreateCurrentService;
|
|
let mut first = FakeScm::new();
|
|
let mut first_snapshot = before.clone();
|
|
first_snapshot.base.display_name = "foreign-one".to_owned();
|
|
first.complete = complete_service(first_snapshot, false);
|
|
let first_fingerprint = observe_proxifyre_cutover_scm_state(&mut first, &operation, &before)
|
|
.expect("first exact unexpected state");
|
|
|
|
let mut repeated = FakeScm::new();
|
|
let mut repeated_snapshot = before.clone();
|
|
repeated_snapshot.base.display_name = "foreign-one".to_owned();
|
|
repeated.complete = complete_service(repeated_snapshot, false);
|
|
let repeated_fingerprint =
|
|
observe_proxifyre_cutover_scm_state(&mut repeated, &operation, &before)
|
|
.expect("repeated exact unexpected state");
|
|
|
|
let mut second = FakeScm::new();
|
|
let mut second_snapshot = before.clone();
|
|
second_snapshot.base.display_name = "foreign-two".to_owned();
|
|
second.complete = complete_service(second_snapshot, false);
|
|
let second_fingerprint = observe_proxifyre_cutover_scm_state(&mut second, &operation, &before)
|
|
.expect("second exact unexpected state");
|
|
|
|
assert_eq!(first_fingerprint, repeated_fingerprint);
|
|
assert_ne!(first_fingerprint, second_fingerprint);
|
|
assert_ne!(
|
|
first_fingerprint,
|
|
expected_proxifyre_cutover_scm_effect(&operation).expect("expected effect")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scm_expected_effect_rejects_untrusted_mutation_rights() {
|
|
let before = before_state();
|
|
let operation = CutoverOperation::CreateCurrentService;
|
|
let mut host = FakeScm::new();
|
|
let mut live = satisfying_scm_observation(&operation, &before, &host.current_base);
|
|
let CompleteServiceObservation::Present { snapshot, .. } = &mut live else {
|
|
panic!("current service fixture must be present");
|
|
};
|
|
snapshot.security.untrusted_mutation_rights = true;
|
|
host.complete = live;
|
|
|
|
assert_ne!(
|
|
observe_proxifyre_cutover_scm_state(&mut host, &operation, &before)
|
|
.expect("exact unsafe SCM observation"),
|
|
expected_proxifyre_cutover_scm_effect(&operation).expect("expected effect")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_current_effect_requires_the_complete_fresh_service_default_profile() {
|
|
let before = before_state();
|
|
let operation = CutoverOperation::CreateCurrentService;
|
|
let mut exact = FakeScm::new();
|
|
exact.complete = satisfying_scm_observation(&operation, &before, &exact.current_base);
|
|
assert_eq!(
|
|
observe_proxifyre_cutover_scm_state(&mut exact, &operation, &before)
|
|
.expect("complete fresh-service defaults"),
|
|
expected_proxifyre_cutover_scm_effect(&operation).expect("expected create effect")
|
|
);
|
|
|
|
let mut drifted = FakeScm::new();
|
|
let mut live = satisfying_scm_observation(&operation, &before, &drifted.current_base);
|
|
let CompleteServiceObservation::Present { snapshot, .. } = &mut live else {
|
|
panic!("current service fixture must be present");
|
|
};
|
|
let description = snapshot
|
|
.config2
|
|
.iter_mut()
|
|
.find(|value| value.kind() == ServiceConfig2Kind::Description)
|
|
.expect("complete default profile");
|
|
*description = ServiceConfig2Snapshot::Description(Some("drift".to_owned()));
|
|
drifted.complete = live;
|
|
assert_ne!(
|
|
observe_proxifyre_cutover_scm_state(&mut drifted, &operation, &before)
|
|
.expect("drifted fresh-service defaults"),
|
|
expected_proxifyre_cutover_scm_effect(&operation).expect("expected create effect")
|
|
);
|
|
}
|
|
|
|
fn complete_service(
|
|
snapshot: ServiceRestoreSnapshot,
|
|
current_dacl_matches: bool,
|
|
) -> CompleteServiceObservation {
|
|
CompleteServiceObservation::Present {
|
|
snapshot: Box::new(snapshot),
|
|
current_dacl_matches,
|
|
}
|
|
}
|
|
|
|
fn satisfying_scm_observation(
|
|
operation: &CutoverOperation,
|
|
before: &ServiceRestoreSnapshot,
|
|
current_base: &ServiceBaseConfigSnapshot,
|
|
) -> CompleteServiceObservation {
|
|
if matches!(
|
|
operation,
|
|
CutoverOperation::DeleteLegacyService | CutoverOperation::DeleteCurrentService
|
|
) {
|
|
return CompleteServiceObservation::Missing;
|
|
}
|
|
|
|
let current = matches!(
|
|
operation,
|
|
CutoverOperation::CreateCurrentService
|
|
| CutoverOperation::SetCurrentServicePolicy(_)
|
|
| CutoverOperation::SetCurrentServiceSecurity
|
|
| CutoverOperation::StartCurrentService
|
|
| CutoverOperation::StopCurrentService
|
|
);
|
|
let mut snapshot = before.clone();
|
|
let mut current_dacl_matches = false;
|
|
if current {
|
|
snapshot.base = current_base.clone();
|
|
snapshot.config2 = SERVICE_CONFIG2_KINDS
|
|
.iter()
|
|
.copied()
|
|
.map(expected_current_proxifyre_service_policy)
|
|
.collect();
|
|
current_dacl_matches = matches!(
|
|
operation,
|
|
CutoverOperation::SetCurrentServiceSecurity
|
|
| CutoverOperation::StartCurrentService
|
|
| CutoverOperation::StopCurrentService
|
|
);
|
|
}
|
|
snapshot.original_state = if matches!(
|
|
operation,
|
|
CutoverOperation::StartCurrentService | CutoverOperation::StartLegacyService
|
|
) {
|
|
ServiceStableState::Running
|
|
} else {
|
|
ServiceStableState::Stopped
|
|
};
|
|
complete_service(snapshot, current_dacl_matches)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn missing_primary_service_config2_probe_is_live_and_read_only() {
|
|
let service = crate::process::query_known_service(KnownWindowsService::Proxifyre)
|
|
.expect("read-only SCM probe");
|
|
if service.exists {
|
|
eprintln!("skipping missing-service assertion because ProxiFyreService exists");
|
|
return;
|
|
}
|
|
let executable = std::env::current_exe().expect("current test executable");
|
|
assert_eq!(
|
|
query_service_config2_exact(
|
|
PROXIFYRE_MANAGED_SERVICE_NAME,
|
|
&executable,
|
|
ServiceConfig2Kind::Description,
|
|
)
|
|
.expect("missing service query"),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
enum CandidateCall {
|
|
CreateRoot,
|
|
WritePackage(PathBuf),
|
|
WriteConfig,
|
|
WriteMarker,
|
|
WriteReceipt,
|
|
}
|
|
|
|
struct FakeCandidateWriter {
|
|
calls: Vec<CandidateCall>,
|
|
fail_on: Option<CandidateCall>,
|
|
fail_after_effect: Option<CandidateCall>,
|
|
observation: ProxifyreCutoverCandidateObservation,
|
|
}
|
|
|
|
impl Default for FakeCandidateWriter {
|
|
fn default() -> Self {
|
|
Self {
|
|
calls: Vec::new(),
|
|
fail_on: None,
|
|
fail_after_effect: None,
|
|
observation: ProxifyreCutoverCandidateObservation::Absent,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FakeCandidateWriter {
|
|
fn record(&mut self, call: CandidateCall) -> Result<(), ProxifyreNativeHostError> {
|
|
self.calls.push(call.clone());
|
|
if self.fail_on.as_ref() == Some(&call) {
|
|
Err(ProxifyreNativeHostError)
|
|
} else if self.fail_after_effect.as_ref() == Some(&call) {
|
|
self.observation = expected_candidate_observation();
|
|
Err(ProxifyreNativeHostError)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ProxifyreCutoverCandidateWriter for FakeCandidateWriter {
|
|
fn observe_candidate(
|
|
&mut self,
|
|
operation: &CutoverOperation,
|
|
) -> Result<ProxifyreCutoverCandidateObservation, ProxifyreNativeHostError> {
|
|
if !matches!(
|
|
operation,
|
|
CutoverOperation::CreateCurrentCandidateRoot
|
|
| CutoverOperation::WriteCurrentCandidatePackageEntry(_)
|
|
| CutoverOperation::WriteCurrentCandidateConfig
|
|
| CutoverOperation::WriteCurrentCandidateMarker
|
|
| CutoverOperation::WriteCurrentCandidateReceipt
|
|
) {
|
|
return Err(ProxifyreNativeHostError);
|
|
}
|
|
Ok(self.observation.clone())
|
|
}
|
|
|
|
fn create_candidate_root(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(CandidateCall::CreateRoot)
|
|
}
|
|
|
|
fn write_candidate_package_entry(
|
|
&mut self,
|
|
relative_path: &Path,
|
|
) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(CandidateCall::WritePackage(relative_path.to_path_buf()))
|
|
}
|
|
|
|
fn write_candidate_config(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(CandidateCall::WriteConfig)
|
|
}
|
|
|
|
fn write_candidate_marker(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(CandidateCall::WriteMarker)
|
|
}
|
|
|
|
fn write_candidate_receipt(&mut self) -> Result<(), ProxifyreNativeHostError> {
|
|
self.record(CandidateCall::WriteReceipt)
|
|
}
|
|
}
|
|
|
|
fn expected_candidate_observation() -> ProxifyreCutoverCandidateObservation {
|
|
let snapshot: SealedPathSnapshot = serde_json::from_value(serde_json::json!({
|
|
"identity": {
|
|
"volumeSerialNumber": 7,
|
|
"fileId": 11,
|
|
"kind": "regular_file",
|
|
"size": 3
|
|
},
|
|
"security": {
|
|
"selfRelative": [1, 2, 3],
|
|
"sacl": "present"
|
|
}
|
|
}))
|
|
.expect("sealed candidate fixture");
|
|
ProxifyreCutoverCandidateObservation::Expected(snapshot)
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_dispatch_selects_exactly_one_create_new_mutation() {
|
|
let mut writer = FakeCandidateWriter::default();
|
|
let relative_path = PathBuf::from("ProxiFyre.exe");
|
|
|
|
assert!(mutate_proxifyre_cutover_candidate(
|
|
&mut writer,
|
|
&CutoverOperation::WriteCurrentCandidatePackageEntry(relative_path.clone()),
|
|
)
|
|
.expect("candidate mutation dispatch"));
|
|
assert_eq!(
|
|
writer.calls,
|
|
vec![CandidateCall::WritePackage(relative_path)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_collision_or_write_failure_is_not_hidden() {
|
|
let mut writer = FakeCandidateWriter {
|
|
fail_on: Some(CandidateCall::WriteReceipt),
|
|
..FakeCandidateWriter::default()
|
|
};
|
|
|
|
mutate_proxifyre_cutover_candidate(
|
|
&mut writer,
|
|
&CutoverOperation::WriteCurrentCandidateReceipt,
|
|
)
|
|
.expect_err("collision/failure must surface");
|
|
assert_eq!(writer.calls, vec![CandidateCall::WriteReceipt]);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_failed_create_or_write_distinguishes_no_effect_from_reacquired_exact_effect() {
|
|
for (operation, call) in [
|
|
(
|
|
CutoverOperation::CreateCurrentCandidateRoot,
|
|
CandidateCall::CreateRoot,
|
|
),
|
|
(
|
|
CutoverOperation::WriteCurrentCandidateReceipt,
|
|
CandidateCall::WriteReceipt,
|
|
),
|
|
] {
|
|
let mut before_effect = FakeCandidateWriter {
|
|
fail_on: Some(call.clone()),
|
|
..FakeCandidateWriter::default()
|
|
};
|
|
mutate_proxifyre_cutover_candidate(&mut before_effect, &operation)
|
|
.expect_err("failure before external effect");
|
|
assert_eq!(
|
|
before_effect
|
|
.observe_candidate(&operation)
|
|
.expect("observe absent target"),
|
|
ProxifyreCutoverCandidateObservation::Absent
|
|
);
|
|
|
|
let mut after_effect = FakeCandidateWriter {
|
|
fail_after_effect: Some(call),
|
|
..FakeCandidateWriter::default()
|
|
};
|
|
mutate_proxifyre_cutover_candidate(&mut after_effect, &operation)
|
|
.expect_err("failure after external effect");
|
|
let observed = after_effect
|
|
.observe_candidate(&operation)
|
|
.expect("reacquire exact target");
|
|
assert!(matches!(
|
|
observed,
|
|
ProxifyreCutoverCandidateObservation::Expected(SealedPathSnapshot {
|
|
identity: safe_fs::StableObjectIdentity {
|
|
volume_serial_number: 7,
|
|
file_id: 11,
|
|
..
|
|
},
|
|
..
|
|
})
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_observer_keeps_unknown_distinct_from_absent_and_expected() {
|
|
let operation = CutoverOperation::CreateCurrentCandidateRoot;
|
|
let mut writer = FakeCandidateWriter {
|
|
observation: ProxifyreCutoverCandidateObservation::Unknown,
|
|
..FakeCandidateWriter::default()
|
|
};
|
|
assert_eq!(
|
|
writer
|
|
.observe_candidate(&operation)
|
|
.expect("typed unknown observation"),
|
|
ProxifyreCutoverCandidateObservation::Unknown
|
|
);
|
|
assert_ne!(
|
|
ProxifyreCutoverCandidateObservation::Unknown,
|
|
ProxifyreCutoverCandidateObservation::Absent
|
|
);
|
|
assert_ne!(
|
|
ProxifyreCutoverCandidateObservation::Unknown,
|
|
expected_candidate_observation()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_dispatch_does_not_claim_scm_or_legacy_filesystem_operations() {
|
|
let mut writer = FakeCandidateWriter::default();
|
|
|
|
assert!(!mutate_proxifyre_cutover_candidate(
|
|
&mut writer,
|
|
&CutoverOperation::CreateCurrentService,
|
|
)
|
|
.expect("non-candidate operation"));
|
|
assert!(writer.calls.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn partial_candidate_handoff_accepts_only_unique_durable_forward_identity() {
|
|
let operation = CutoverOperation::CreateCurrentCandidateRoot;
|
|
let identity = safe_fs::StableObjectIdentity {
|
|
volume_serial_number: 7,
|
|
file_id: 11,
|
|
kind: safe_fs::StableObjectKind::Directory,
|
|
size: 0,
|
|
};
|
|
let fingerprint = StateFingerprint::digest("candidate-handoff-test", b"state");
|
|
let durable = MutationRecord {
|
|
sequence: 0,
|
|
direction: MutationDirection::Forward,
|
|
operation: operation.clone(),
|
|
before_state: fingerprint.clone(),
|
|
expected_effect: fingerprint.clone(),
|
|
intent_written_at_epoch_seconds: 1,
|
|
authority_evidence: None,
|
|
effect: Some(MutationEffect {
|
|
disposition: EffectDisposition::ExpectedEffect,
|
|
observed: fingerprint,
|
|
object_identity: Some(identity.clone()),
|
|
observed_at_epoch_seconds: 2,
|
|
}),
|
|
};
|
|
|
|
assert_eq!(
|
|
unique_forward_expected_effect_identity(std::slice::from_ref(&durable), &operation)
|
|
.expect("unique durable identity"),
|
|
Some(&identity)
|
|
);
|
|
|
|
let mut pending = durable.clone();
|
|
pending.effect = None;
|
|
assert_eq!(
|
|
unique_forward_expected_effect_identity(&[pending], &operation)
|
|
.expect("pending intent is not durable effect"),
|
|
None
|
|
);
|
|
|
|
let mut missing_identity = durable.clone();
|
|
missing_identity.effect.as_mut().unwrap().object_identity = None;
|
|
assert!(unique_forward_expected_effect_identity(&[missing_identity], &operation).is_err());
|
|
assert!(
|
|
unique_forward_expected_effect_identity(&[durable.clone(), durable], &operation).is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn prepared_candidate_freezes_complete_sorted_final_metadata() {
|
|
let (plan, runtime, config, config_sha256) = candidate_inputs();
|
|
let prepared = prepare_proxifyre_cutover_candidate(
|
|
&plan,
|
|
runtime,
|
|
&config,
|
|
&config_sha256,
|
|
false,
|
|
1_700_000_000,
|
|
)
|
|
.expect("prepare complete cutover candidate");
|
|
|
|
assert_eq!(
|
|
prepared.snapshot().files.len(),
|
|
CURRENT_PROXIFYRE_PACKAGE_FILES.len() + 3
|
|
);
|
|
assert!(valid_sha256(&prepared.snapshot().manifest_fingerprint));
|
|
assert!(prepared.snapshot().files.windows(2).all(|pair| {
|
|
candidate_relative_label(&pair[0].relative_path)
|
|
< candidate_relative_label(&pair[1].relative_path)
|
|
}));
|
|
let config_spec = prepared
|
|
.file_spec(Path::new("app-config.json"))
|
|
.expect("config spec");
|
|
assert_eq!(config_spec.role, CurrentCandidateFileRole::Config);
|
|
assert_eq!(config_spec.sha256, config_sha256);
|
|
|
|
let marker: SystemProxifyreMarker = serde_json::from_slice(
|
|
prepared
|
|
.file_bytes(Path::new(PROXIFYRE_MARKER_FILE))
|
|
.expect("marker bytes"),
|
|
)
|
|
.expect("marker JSON");
|
|
assert!(marker.packet_filter_installed_by_proxy_warden);
|
|
let receipt: InstallReceipt = serde_json::from_slice(
|
|
prepared
|
|
.file_bytes(Path::new(INSTALL_RECEIPT_FILENAME))
|
|
.expect("receipt bytes"),
|
|
)
|
|
.expect("receipt JSON");
|
|
assert_eq!(receipt.installed_at, 1_700_000_000);
|
|
assert!(receipt
|
|
.windows_packet_filter
|
|
.as_ref()
|
|
.is_some_and(|ownership| ownership.installed_by_proxy_warden));
|
|
|
|
let (_, repeated_runtime, _, _) = candidate_inputs_with_plan(&plan);
|
|
let repeated = prepare_proxifyre_cutover_candidate(
|
|
&plan,
|
|
repeated_runtime,
|
|
&config,
|
|
&config_sha256,
|
|
false,
|
|
1_700_000_000,
|
|
)
|
|
.expect("repeat identical candidate");
|
|
assert_eq!(
|
|
prepared.snapshot().manifest_fingerprint,
|
|
repeated.snapshot().manifest_fingerprint
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn captured_timestamp_and_preexisting_packet_filter_change_final_manifest() {
|
|
let (plan, runtime, config, config_sha256) = candidate_inputs();
|
|
let first = prepare_proxifyre_cutover_candidate(
|
|
&plan,
|
|
runtime,
|
|
&config,
|
|
&config_sha256,
|
|
false,
|
|
1_700_000_000,
|
|
)
|
|
.expect("first candidate");
|
|
let (_, runtime, _, _) = candidate_inputs_with_plan(&plan);
|
|
let second = prepare_proxifyre_cutover_candidate(
|
|
&plan,
|
|
runtime,
|
|
&config,
|
|
&config_sha256,
|
|
true,
|
|
1_700_000_001,
|
|
)
|
|
.expect("second candidate");
|
|
|
|
assert_ne!(
|
|
first.snapshot().manifest_fingerprint,
|
|
second.snapshot().manifest_fingerprint
|
|
);
|
|
let receipt: InstallReceipt = serde_json::from_slice(
|
|
second
|
|
.file_bytes(Path::new(INSTALL_RECEIPT_FILENAME))
|
|
.expect("receipt bytes"),
|
|
)
|
|
.expect("receipt JSON");
|
|
assert!(receipt.windows_packet_filter.is_none());
|
|
}
|
|
|
|
fn candidate_inputs() -> (
|
|
ProxifyreCutoverPlan,
|
|
PreparedProxifyreRuntime,
|
|
Vec<u8>,
|
|
String,
|
|
) {
|
|
let app_root = std::env::temp_dir().join("proxywarden-cutover-contract");
|
|
let config = br#"{"proxies":[],"applications":[]}"#.to_vec();
|
|
let config_sha256 = format!("{:x}", Sha256::digest(&config));
|
|
let package_sha256 = "a".repeat(64);
|
|
let plan = ProxifyreCutoverPlan::new(
|
|
&app_root,
|
|
PathBuf::from(r"C:\Tools\ProxiFyre"),
|
|
LegacyServiceState::Stopped,
|
|
"2.2.1".to_owned(),
|
|
package_sha256,
|
|
config_sha256.clone(),
|
|
"b".repeat(64),
|
|
uuid::Uuid::new_v4().hyphenated().to_string(),
|
|
);
|
|
let (_, runtime, _, _) = candidate_inputs_with_plan(&plan);
|
|
(plan, runtime, config, config_sha256)
|
|
}
|
|
|
|
fn candidate_inputs_with_plan(
|
|
plan: &ProxifyreCutoverPlan,
|
|
) -> (
|
|
ProxifyreCutoverPlan,
|
|
PreparedProxifyreRuntime,
|
|
Vec<u8>,
|
|
String,
|
|
) {
|
|
let files: Vec<_> = CURRENT_PROXIFYRE_PACKAGE_FILES
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, name)| {
|
|
let bytes = vec![u8::try_from(index + 1).expect("small fixture index")];
|
|
ProxifyreStagedFile {
|
|
relative_path: (*name).to_owned(),
|
|
sha256: format!("{:x}", Sha256::digest(&bytes)),
|
|
size: bytes.len() as u64,
|
|
bytes,
|
|
}
|
|
})
|
|
.collect();
|
|
let runtime = PreparedProxifyreRuntime {
|
|
proof: PrivilegedPackageProof {
|
|
component_id: ComponentId::Proxifyre,
|
|
version: plan.bundled_version.clone(),
|
|
asset_name: "proxifyre.zip".to_owned(),
|
|
sha256: plan.package_fingerprint.clone(),
|
|
size: 123,
|
|
source: PackageSource::Bundled,
|
|
independent_proof: None,
|
|
},
|
|
installed_files: installed_file_inventory(&files),
|
|
files,
|
|
};
|
|
let config = br#"{"proxies":[],"applications":[]}"#.to_vec();
|
|
let config_sha256 = format!("{:x}", Sha256::digest(&config));
|
|
(plan.clone(), runtime, config, config_sha256)
|
|
}
|