@@ -0,0 +1,921 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user