use fingerprintd::fuzzy::{Decision, FuzzyStore, VelocityBand};
use serde_json::{Value, json};
fn full_probe() -> Value {
json!({
"webgl": "ANGLE (Intel)",
"platform": "Linux x86_64",
"timezone": "Asia/Shanghai",
"audio": "124.04",
"cpu_cores": 8,
"device_memory": 8,
"fonts": ["Arial", "Helvetica", "Courier", "Times", "Verdana"],
"user_agent": "Chrome/120",
})
}
fn distinct_probe(i: u64) -> Value {
json!({
"webgl": format!("ANGLE (Vendor {i})"),
"platform": format!("Platform-{i}"),
"timezone": format!("Zone/{i}"),
"audio": format!("12{i}.5"),
"cpu_cores": 4 + i,
"device_memory": 4 + i,
"fonts": [
format!("F{i}a"), format!("F{i}b"), format!("F{i}c"),
format!("F{i}d"), format!("F{i}e"),
],
"user_agent": format!("Agent/{i}"),
})
}
#[test]
fn a_single_component_drift_stays_the_same_device() {
let store = FuzzyStore::new();
let first = store.identify(&full_probe(), 1_000);
assert!(first.is_new_device);
let mut drifted = full_probe();
drifted["user_agent"] = json!("Chrome/121");
drifted["fonts"] = json!(["Arial", "Helvetica", "Courier", "Times", "Segoe"]);
let second = store.identify(&drifted, 2_000);
assert_eq!(second.decision, Decision::Match);
assert!(!second.is_new_device);
assert_eq!(second.visitor_id, first.visitor_id);
assert!(
second.confidence > 0.7,
"confidence was {}",
second.confidence
);
}
#[test]
fn d_missing_components_neither_collide_nor_inflate_confidence() {
let privacy = FuzzyStore::new();
let a = privacy.identify(
&json!({ "webgl": "GPU-A", "platform": "Linux", "timezone": "UTC", "canvas": null }),
1_000,
);
let b = privacy.identify(
&json!({ "webgl": "GPU-B", "platform": "Windows", "timezone": "GMT", "canvas": null }),
2_000,
);
assert!(a.is_new_device && b.is_new_device);
assert_ne!(a.visitor_id, b.visitor_id);
let store = FuzzyStore::new();
store.identify(&full_probe(), 1_000);
let full = store.identify(&full_probe(), 2_000);
assert_eq!(full.decision, Decision::Match);
let sparse = json!({
"webgl": "ANGLE (Intel)",
"platform": "Linux x86_64",
"timezone": "Asia/Shanghai",
});
let partial = store.identify(&sparse, 3_000);
assert!(partial.compared_components < full.compared_components);
assert!(
partial.confidence < full.confidence,
"partial {} should be below full {}",
partial.confidence,
full.confidence,
);
}
#[test]
fn fresh_seed_per_launch_each_mints_a_new_device() {
use std::collections::HashSet;
let store = FuzzyStore::new();
let launches = [
json!({
"webgl": "ANGLE (NVIDIA GeForce RTX 3060)",
"platform": "Win32",
"timezone": "America/Chicago",
"audio": "124.11",
"cpu_cores": 12,
"device_memory": 16,
"fonts": ["Arial", "Calibri", "Segoe UI", "Tahoma"],
"user_agent": "Chrome/126",
}),
json!({
"webgl": "ANGLE (AMD Radeon RX 6800)",
"platform": "MacIntel",
"timezone": "Europe/Berlin",
"audio": "121.37",
"cpu_cores": 8,
"device_memory": 8,
"fonts": ["Helvetica", "SF Pro", "Menlo", "Geneva"],
"user_agent": "Chrome/126",
}),
json!({
"webgl": "ANGLE (Intel Iris Xe)",
"platform": "Linux x86_64",
"timezone": "Asia/Tokyo",
"audio": "119.82",
"cpu_cores": 4,
"device_memory": 4,
"fonts": ["Roboto", "Noto Sans", "Ubuntu", "DejaVu Sans"],
"user_agent": "Chrome/126",
}),
];
let mut ids = HashSet::new();
for (i, probe) in launches.iter().enumerate() {
let out = store.identify(probe, 1_000 + i as u64 * 1_000);
assert_eq!(out.decision, Decision::NewDevice, "launch {i}");
assert!(out.is_new_device, "launch {i} should mint a new device");
assert!(
ids.insert(out.visitor_id),
"launch {i} produced a duplicate visitor_id",
);
}
assert_eq!(ids.len(), launches.len());
}
#[test]
fn no_client_ip_stays_low_and_neutral() {
let store = FuzzyStore::new();
let probe = distinct_probe(1);
let out = store.identify_with_ip(&probe, 1_000_000, None);
assert_eq!(out.new_device_velocity, VelocityBand::Low);
assert!((out.confidence - 0.5).abs() < 1e-9);
let other = distinct_probe(2);
let banded = store.identify_with_ip(&other, 1_000_100, Some("198.51.100.7"));
assert_eq!(banded.new_device_velocity, VelocityBand::Low);
}
#[test]
fn cold_start_is_neutral_and_identify_delegates_to_no_ip() {
let probe = full_probe();
let via_identify = FuzzyStore::new().identify(&probe, 1_000);
let via_no_ip = FuzzyStore::new().identify_with_ip(&probe, 1_000, None);
assert_eq!(via_identify.decision, Decision::NewDevice);
assert_eq!(via_identify.new_device_velocity, VelocityBand::Low);
assert_eq!(via_identify.decision, via_no_ip.decision);
assert_eq!(via_identify.visitor_id, via_no_ip.visitor_id);
assert_eq!(via_identify.is_new_device, via_no_ip.is_new_device);
assert!((via_identify.confidence - via_no_ip.confidence).abs() < f64::EPSILON);
}