src/support/tests.rs
Rama main · réplica de lectura del repositorio original.
use super::privacy;
use serde_json::json;
#[test]
fn malicious_reports_configurations_and_errors_are_allowlisted() {
let malicious = json!({
"username": "private-apple-id", "password": "private-password", "token": "private-token", "cookie": "private-cookie",
"download_dir": "/private/path", "albums": ["private-album"], "library": "private-zone", "checksum": "private-checksum",
"resolution": "private-provider-response", "inventory_drop_library": "private-library", "sync_token_blocked_zone": "private-zone",
"sync_token_blocked_explanation": "private-URL", "sync_token_blocked_reason": "private-error",
"bytes_downloaded": 123, "disk_bytes_written": 456, "threads": 2,
"primary_layout": {"bound_families": 4, "private": "private-id"},
});
let stats = privacy::stats(&malicious);
let configuration = privacy::configuration(&malicious);
assert_eq!(stats["bytes_downloaded"], 123);
assert_eq!(stats["disk_bytes_written"], 456);
assert_eq!(configuration["threads"], 2);
assert!(!format!("{stats}{configuration}").contains("private"));
let error = anyhow::anyhow!(
"https://private-url /private-path password=private-token AppleID=private-apple-id"
);
let fields = super::classify_support_error("download", &error);
assert_eq!(fields["class"], "other");
assert!(!fields.to_string().contains("private"));
}
#[test]
fn historical_support_questions_have_safe_structured_answers() {
// Each fixture states the actual historical question and concrete safe
// answer fields, rather than equating a counter collection with diagnosis.
let cases = [
(
"765 external restart",
"support_startup_v1",
json!({"phase":"shutdown","outcome":"interrupted","interrupted":true}),
),
(
"853 config provenance",
"support_startup_v1",
json!({"phase":"configuration","outcome":"ready"}),
),
(
"770 query completion",
"support_pass_completion_v1",
json!({"completion":"proven_eof","selected_scope":"selected","selection_comparable":false}),
),
(
"765 receiver mismatch",
"support_pass_completion_v1",
json!({"expected_fetchers":4,"completed_fetchers":4,"token_count":4,"unique_token_count":2}),
),
(
"924 bridge successor",
"support_checkpoint_v1",
json!({"bridge":true,"successor_provenance":"provider","token_present":true,"reason":"veto"}),
),
(
"925 inventory comparability",
"support_pass_completion_v1",
json!({"api_total":604,"assets_seen":17,"inventory_complete":false,"selection_comparable":false}),
),
("765 sparse debt", "sparse_identity_state_failed", json!({})),
(
"845 owner morphology",
"exact_lookup_rejection_v1",
json!({"target":"paired","stage":"master_reference_scope","reason":"owner_unqualified","expected_owner":"absent","lookup_zone":"primary","rejected_requests":604,"supplied_owner":"other_string","supplied_owner_type":"string","owner_provenance":"lookup_request"}),
),
(
"853 ambiguity",
"metadata_capture_ambiguity_counts_v1",
json!({"stored_renditions":2,"matching_children":3,"full_evidence_matching_children":1}),
),
(
"845 inventory unrelated scope",
"legacy_inventory_failure_v2",
json!({"reason":"invalid_inventory_evidence","phase":"family_scan","subreason":"child_reference_scope_mismatch","eof_observed":true,"family_context":"unrelated","scope_mismatch_component":"owner","expected_owner":"absent","expected_owner_type":"absent","supplied_owner":"private_default","supplied_owner_type":"string","pages":5,"records":604,"transferred_bytes":40000000,"retained_bytes":25000000,"retained_byte_budget":268435456,"elapsed_secs":1020.0}),
),
(
"845 refresh omissions",
"exact_refresh_outcomes_v1",
json!({"requested_task_keys":604,"refreshed_task_keys":17,"remaining_task_keys":587,"phase_elapsed_secs":4.0,"oldest_refreshed_url_observed_age_secs":8.0}),
),
(
"845 receipt reuse",
"support_publication_v1",
json!({"operation":"receipt_recovery","receipt_match":true,"content_equal":true,"size_equal":true,"publication_time_retained":true,"actual_publication":false,"source_status":"pending","destination_status":"downloaded"}),
),
(
"858 filesystem error",
"support_task_error_v1",
json!({"operation":"publication","class":"io","errno":95}),
),
(
"914 first attempt before eventual success",
"support_transfer_pass_v1",
json!({"phase":"first","failed":17,"downloaded":604,"concurrency":2,"state_write_failures":0}),
),
(
"913 strict import",
"support_import_v1",
json!({"phase":"adoption","reason":"strict_refusal","dry_run":true}),
),
(
"929 precision",
"support_metadata_v1",
json!({"operation":"embed","backend":"native_exif","source_fractional":true,"planned_fractional":true,"written_fractional":false,"verified":true}),
),
(
"926 provider readiness",
"support_startup_v1",
json!({"phase":"startup","class":"provider_not_ready","http_status":null}),
),
];
for (question, kind, fields) in cases {
let clean = privacy::diagnostic(kind, fields.as_object().unwrap()).unwrap();
assert_eq!(
serde_json::Value::Object(clean),
fields,
"missing actionable evidence for {question}"
);
}
}
#[test]
fn tim_paired_counts_remain_overlapping_observations() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("history.json");
// Export never sums 604/17/604 into a unique asset count.
let mut cycle = json!({"id":uuid::Uuid::new_v4().to_string(),"started_at":"2026-10-10T16:00:00Z","completed_at":null,
"outcome":"running","build_version":"0.25.1-dev","configuration":{},"stats":{},"diagnostics":[]});
cycle["diagnostics"] = json!([604,17,604].into_iter().map(|n| json!({"kind":"exact_lookup_rejection_v1",
"fields":{"target":"paired","stage":"master_reference_scope","reason":"owner_unqualified","rejected_requests":n},
"observations":1,"first_at":"2026-10-10T16:01:00Z","last_at":"2026-10-10T16:01:00Z"})).collect::<Vec<_>>());
std::fs::write(&path, serde_json::to_vec(&json!({"schema_version":1,"cycles_evicted":0,"queue_dropped":0,"groups_omitted":0,"previous_history_unavailable":false,"cycles":[cycle]})).unwrap()).unwrap();
let (h, status) = super::history::read(&path);
assert_eq!(status, "available");
assert_eq!(h.cycles[0].diagnostics.len(), 3);
assert_eq!(h.cycles[0].diagnostics[1].fields["rejected_requests"], 17);
}
#[test]
fn owning_checkpoint_decisions_survive_support_allowlist_without_tokens() {
use crate::download::RecoveryAction;
use crate::sync_cycle::{
CheckpointBasis, CheckpointHoldReason, SourceCheckpointDecision, checkpoint_support_fields,
};
for reason in [
CheckpointHoldReason::DryRun,
CheckpointHoldReason::StalePassPlan,
CheckpointHoldReason::Interrupted,
CheckpointHoldReason::SessionExpired,
CheckpointHoldReason::EnumerationIncomplete,
CheckpointHoldReason::StateNotDurable,
CheckpointHoldReason::TokenProofIncomplete,
CheckpointHoldReason::LegacyPreservationIncomplete,
] {
let decision = SourceCheckpointDecision::Preserve {
reason,
recovery: RecoveryAction::ReplayFromPriorToken,
};
let fields = checkpoint_support_fields(&decision);
let projected =
super::privacy::diagnostic("support_checkpoint_v1", fields.as_object().unwrap())
.unwrap();
assert_eq!(serde_json::Value::Object(projected), fields);
assert_eq!(fields["decision"], "preserved");
}
for basis in [
CheckpointBasis::IncrementalDelta,
CheckpointBasis::CompleteInventory,
CheckpointBasis::InventoryWithDeltaBridge,
] {
let fields = checkpoint_support_fields(&SourceCheckpointDecision::Advance {
token: "PRIVATE_PROVIDER_TOKEN".into(),
basis,
});
let projected =
super::privacy::diagnostic("support_checkpoint_v1", fields.as_object().unwrap())
.unwrap();
assert_eq!(serde_json::Value::Object(projected), fields);
assert!(!fields.to_string().contains("PRIVATE"));
}
for persistence in [
"failed",
"stored",
"stored_hold",
"staged_reconciliation",
"no_state_db",
] {
let fields = serde_json::json!({"persistence": persistence});
assert_eq!(
serde_json::Value::Object(
super::privacy::diagnostic("support_checkpoint_v1", fields.as_object().unwrap())
.unwrap()
),
fields
);
}
assert_eq!(
super::privacy::stats(
&serde_json::json!({"sync_token_blocked_reason":"inventory_delta_bridge_failed"})
)["sync_token_blocked_reason"],
"inventory_delta_bridge_failed"
);
}
#[test]
fn existing_refresh_hold_and_hardlink_fixed_fields_survive_production_layer() {
use tracing_subscriber::layer::SubscriberExt;
let (layer, drain) = super::history::test_layer();
tracing::subscriber::with_default(tracing_subscriber::registry().with(layer), || {
tracing::warn!(
diagnostic = "expired_url_refresh_failed",
failed_records = 17u64,
authentication_failures = 2u64,
rate_limit_observations = 3u64,
error = "PRIVATE_TOKEN"
);
tracing::warn!(
diagnostic = "retained_checkpoint_expired",
retry_deferred = true,
state_write_failed = false,
retry_exhausted = true,
zone = "PRIVATE_ZONE"
);
tracing::warn!(
diagnostic = "legacy_preservation_hardlink_trust",
affected_paths = 4u64
);
});
let evidence = drain();
assert_eq!(evidence.len(), 3);
assert_eq!(
evidence[0]["fields"],
json!({"failed_records":17,"authentication_failures":2,"rate_limit_observations":3})
);
assert_eq!(
evidence[1]["fields"],
json!({"retry_deferred":true,"state_write_failed":false,"retry_exhausted":true})
);
assert_eq!(evidence[2]["fields"], json!({"affected_paths":4}));
assert!(
!serde_json::to_string(&evidence)
.unwrap()
.contains("PRIVATE")
);
}
#[test]
fn typed_effective_configuration_preserves_every_file_raw_and_live_mode() {
use crate::types::{FileMatchPolicy, LivePhotoMode, RawPolicy};
let dir = tempfile::tempdir().unwrap();
let globals = crate::config::GlobalArgs {
username: Some("PRIVATE_USER".into()),
domain: None,
data_dir: Some(dir.path().to_string_lossy().into_owned()),
};
let toml: crate::config::TomlConfig = toml::from_str(&format!(
"[download]\ndirectory={:?}",
dir.path().to_string_lossy()
))
.unwrap();
let mut config = crate::config::Config::build(
&globals,
&crate::cli::PasswordArgs::default(),
crate::cli::SyncArgs::default(),
Some(&toml),
)
.unwrap();
for (variant, label) in [
(
FileMatchPolicy::NameSizeDedupWithSuffix,
"name-size-dedup-with-suffix",
),
(FileMatchPolicy::NameId7, "name-id7"),
] {
config.photos.file_match_policy = variant;
assert_eq!(
super::runtime_configuration(&config)["file_match_policy"],
label
);
}
for (variant, label) in [
(RawPolicy::AsIs, "as-is"),
(RawPolicy::PreferRaw, "prefer-raw"),
(RawPolicy::PreferJpeg, "prefer-jpeg"),
] {
config.photos.raw_policy = variant;
assert_eq!(super::runtime_configuration(&config)["raw_policy"], label);
}
for (variant, label) in [
(LivePhotoMode::Both, "both"),
(LivePhotoMode::ImageOnly, "image-only"),
(LivePhotoMode::VideoOnly, "video-only"),
(LivePhotoMode::Skip, "skip"),
] {
config.photos.live_photo_mode = variant;
assert_eq!(
super::runtime_configuration(&config)["live_photo_mode"],
label
);
let input: crate::config::TomlConfig =
toml::from_str(&format!("[photos]\nlive_photo_mode={label:?}")).unwrap();
assert_eq!(
super::configuration(Some(&input), true)["live_photo_mode"],
label
);
}
assert!(
!super::runtime_configuration(&config)
.to_string()
.contains("PRIVATE")
);
}
#[test]
fn hydration_wait_contract_retains_only_fixed_phases_outcomes_and_unsigned_time() {
for phase in ["asset_identity", "paired_asset", "album_relation"] {
for outcome in ["started", "waiting", "returned", "cancelled"] {
let fields = json!({"phase": phase, "outcome": outcome, "elapsed_secs": 31,
"token": "private-token", "library": "private-zone", "message": "private-body"});
let safe =
privacy::diagnostic("support_hydration_wait_v1", fields.as_object().unwrap())
.unwrap();
assert_eq!(
serde_json::Value::Object(safe),
json!({"phase": phase, "outcome": outcome, "elapsed_secs": 31})
);
}
}
let fields =
json!({"phase": "private-zone", "outcome": "private-response", "elapsed_secs": -1});
assert!(
privacy::diagnostic("support_hydration_wait_v1", fields.as_object().unwrap())
.unwrap()
.is_empty()
);
}
#[test]
fn missing_reference_contract_rejects_private_and_numeric_categories() {
let expected = json!({"record_history":"replacement", "prior_reference":"present",
"sparse_marker":"true", "linkage":"all_fields", "lookup_outcome":"live_reference", "lookup_family":"candidate", "lookup_scope":"matched", "lookup_shape":"live_reference"});
assert_eq!(
serde_json::Value::Object(
privacy::diagnostic(
"support_missing_reference_v1",
expected.as_object().unwrap()
)
.unwrap()
),
expected
);
for value in [
json!(42),
json!("private-provider-value"),
json!({"nested":"private"}),
json!(null),
] {
let fields = expected
.as_object()
.unwrap()
.keys()
.map(|key| (key.clone(), value.clone()))
.collect();
assert!(
privacy::diagnostic("support_missing_reference_v1", &fields)
.unwrap()
.is_empty()
);
}
}
#[test]
fn issue956_lookup_record_status_contract_is_closed_and_optional() {
for label in [
"not_requested",
"omitted",
"returned",
"tombstone",
"not_found",
"access_denied",
"authentication_failed",
"zone_not_found",
"retry_later",
"throttled",
"server_error",
"other_error",
"malformed_error",
] {
let fields = json!({"master_status":label,"child_status":label});
assert_eq!(
privacy::diagnostic("exact_lookup_rejection_v1", fields.as_object().unwrap()).unwrap(),
fields.as_object().unwrap().clone()
);
}
for invalid in [
json!("ACCESS_DENIED"),
json!("PRIVATE_ERROR_CANARY"),
json!(401),
json!(null),
json!({"secret":"PRIVATE"}),
] {
let fields = json!({"master_status":invalid,"child_status":invalid,"reason":"record_provider_error"});
let clean =
privacy::diagnostic("exact_lookup_rejection_v1", fields.as_object().unwrap()).unwrap();
assert_eq!(
clean,
json!({"reason":"record_provider_error"})
.as_object()
.unwrap()
.clone()
);
}
// Older retained history carries no newly invented response evidence.
let fields = json!({"target":"paired","reason":"record_provider_error"});
assert_eq!(
privacy::diagnostic("exact_lookup_rejection_v1", fields.as_object().unwrap()).unwrap(),
fields.as_object().unwrap().clone()
);
}
#[test]
fn issue956_child_reference_status_is_a_field_specific_allowlist() {
for label in [
"not_requested",
"omitted",
"unavailable",
"invalid_child",
"missing",
"malformed",
"sparse",
"matches_requested",
"different",
] {
let fields = json!({"child_reference_status":label});
assert_eq!(
privacy::diagnostic("exact_lookup_rejection_v1", fields.as_object().unwrap()).unwrap(),
*fields.as_object().unwrap()
);
}
for invalid in [
json!("PRIVATE_REFERENCE_CANARY"),
json!("returned"),
json!(42),
json!(null),
json!(true),
json!({"recordName":"PRIVATE"}),
] {
let fields = json!({"child_reference_status":invalid,"reason":"record_provider_error"});
let clean =
privacy::diagnostic("exact_lookup_rejection_v1", fields.as_object().unwrap()).unwrap();
assert_eq!(
clean,
*json!({"reason":"record_provider_error"})
.as_object()
.unwrap()
);
}
let old = json!({"target":"paired","reason":"record_provider_error"});
let clean = privacy::diagnostic("exact_lookup_rejection_v1", old.as_object().unwrap()).unwrap();
assert_eq!(
clean,
*old.as_object().unwrap(),
"old history cannot invent reference evidence"
);
}
#[test]
fn recovery_decisions_preserve_only_fixed_evidence_outcomes() {
for (decision, reason) in [
("partial", "historical_claim_prepared"),
("unsupported", "historical_claim_unverified"),
] {
let fields = json!({
"mechanism": "historical_path", "decision": decision, "reason": reason,
"owner": "private-owner", "path": "/private/media", "record": "private-record",
"token": "private-token", "url": "https://private-provider"
});
let projected =
privacy::diagnostic("recovery_decision_v1", fields.as_object().unwrap()).unwrap();
assert_eq!(
json!(projected),
json!({"mechanism":"historical_path","decision":decision,"reason":reason})
);
}
let fields =
json!({"mechanism":"private-owner","decision":"private-token","reason":"private-path"});
let projected =
privacy::diagnostic("recovery_decision_v1", fields.as_object().unwrap()).unwrap();
assert!(
projected.is_empty(),
"saved history must not authorize arbitrary labels"
);
}