use super::*;
fn user(s: &str) -> UserId {
axess_identity::testing::user(s)
}
fn tenant(s: &str) -> TenantId {
axess_identity::testing::tenant(s)
}
#[test]
fn token_wire_format_is_pii_colon_uuid() {
let uuid = Uuid::nil();
let token = PiiToken::from_uuid(uuid);
assert_eq!(token.as_str(), "pii:00000000-0000-0000-0000-000000000000");
let parsed = PiiToken::parse(token.as_str()).expect("round-trip parse");
assert_eq!(parsed, token);
}
#[test]
fn is_well_formed_rejects_malformed() {
assert!(!PiiToken::is_well_formed(
"00000000-0000-0000-0000-000000000000"
));
assert!(!PiiToken::is_well_formed(
"pii_00000000-0000-0000-0000-000000000000"
));
assert!(!PiiToken::is_well_formed("pii:not-a-uuid"));
assert!(!PiiToken::is_well_formed(""));
assert!(!PiiToken::is_well_formed("pii:"));
assert!(PiiToken::is_well_formed(
"pii:00000000-0000-0000-0000-000000000000"
));
}
#[tokio::test]
async fn record_then_resolve_round_trips_value() {
let store = MemoryDevicePiiStore::new();
let now = Utc::now();
let token = store
.record(
&user("u1"),
&tenant("t1"),
DevicePiiCategory::DisplayName,
"Alice's iPhone".to_string(),
now,
)
.await
.unwrap();
assert!(PiiToken::is_well_formed(token.as_str()));
let value = store.resolve(&token, &tenant("t1")).await.unwrap();
assert_eq!(value.as_deref(), Some("Alice's iPhone"));
}
#[tokio::test]
async fn resolve_refuses_cross_tenant_lookup() {
let store = MemoryDevicePiiStore::new();
let token = store
.record(
&user("u1"),
&tenant("alpha"),
DevicePiiCategory::IpAddress,
"203.0.113.42".to_string(),
Utc::now(),
)
.await
.unwrap();
let same = store.resolve(&token, &tenant("alpha")).await.unwrap();
assert_eq!(same.as_deref(), Some("203.0.113.42"));
let cross = store.resolve(&token, &tenant("beta")).await.unwrap();
assert!(
cross.is_none(),
"cross-tenant lookup must be invisible, not leak the value"
);
}
#[tokio::test]
async fn erase_subject_removes_only_target_subject_mappings() {
let store = MemoryDevicePiiStore::new();
let now = Utc::now();
let t = tenant("t1");
let alice_dn = store
.record(
&user("alice"),
&t,
DevicePiiCategory::DisplayName,
"Alice".into(),
now,
)
.await
.unwrap();
let alice_ip = store
.record(
&user("alice"),
&t,
DevicePiiCategory::IpAddress,
"203.0.113.1".into(),
now,
)
.await
.unwrap();
let bob_dn = store
.record(
&user("bob"),
&t,
DevicePiiCategory::DisplayName,
"Bob".into(),
now,
)
.await
.unwrap();
let erased = store.erase_subject(&user("alice"), &t).await.unwrap();
assert_eq!(erased, 2, "must erase exactly Alice's 2 rows");
assert!(store.resolve(&alice_dn, &t).await.unwrap().is_none());
assert!(store.resolve(&alice_ip, &t).await.unwrap().is_none());
assert_eq!(
store.resolve(&bob_dn, &t).await.unwrap().as_deref(),
Some("Bob"),
);
}
#[tokio::test]
async fn erase_subject_is_tenant_scoped() {
let store = MemoryDevicePiiStore::new();
let alice_alpha = store
.record(
&user("alice"),
&tenant("alpha"),
DevicePiiCategory::DisplayName,
"Alice@alpha".into(),
Utc::now(),
)
.await
.unwrap();
let alice_beta = store
.record(
&user("alice"),
&tenant("beta"),
DevicePiiCategory::DisplayName,
"Alice@beta".into(),
Utc::now(),
)
.await
.unwrap();
let erased = store
.erase_subject(&user("alice"), &tenant("alpha"))
.await
.unwrap();
assert_eq!(erased, 1);
assert!(
store
.resolve(&alice_alpha, &tenant("alpha"))
.await
.unwrap()
.is_none()
);
assert_eq!(
store
.resolve(&alice_beta, &tenant("beta"))
.await
.unwrap()
.as_deref(),
Some("Alice@beta"),
"erasure in tenant alpha must not touch tenant beta"
);
}
#[tokio::test]
async fn list_for_subject_returns_all_mappings_in_tenant() {
let store = MemoryDevicePiiStore::new();
let t = tenant("t1");
let now = Utc::now();
store
.record(
&user("alice"),
&t,
DevicePiiCategory::DisplayName,
"Alice".into(),
now,
)
.await
.unwrap();
store
.record(
&user("alice"),
&t,
DevicePiiCategory::UserAgentString,
"Mozilla/5.0".into(),
now,
)
.await
.unwrap();
store
.record(
&user("bob"),
&t,
DevicePiiCategory::DisplayName,
"Bob".into(),
now,
)
.await
.unwrap();
let alice_rows = store.list_for_subject(&user("alice"), &t).await.unwrap();
assert_eq!(
alice_rows.len(),
2,
"portability export must include all of Alice's mappings"
);
assert!(
alice_rows.iter().all(|m| m.subject_id == user("alice")),
"list_for_subject must not leak other subjects' rows"
);
}
#[tokio::test]
async fn resolver_returns_redacted_for_missing_token() {
let store = MemoryDevicePiiStore::new();
let bogus = PiiToken::new();
let resolved = store
.resolve_or_redacted(&bogus, &tenant("t1"))
.await
.unwrap();
assert_eq!(resolved, REDACTED_PLACEHOLDER);
}
#[tokio::test]
async fn resolver_returns_real_value_when_present() {
let store = MemoryDevicePiiStore::new();
let token = store
.record(
&user("u1"),
&tenant("t1"),
DevicePiiCategory::DisplayName,
"Alice".into(),
Utc::now(),
)
.await
.unwrap();
let resolved = store
.resolve_or_redacted(&token, &tenant("t1"))
.await
.unwrap();
assert_eq!(resolved, "Alice");
}
#[tokio::test]
async fn redacted_resolver_unconditionally_returns_placeholder() {
let resolver = RedactedResolver;
let token = PiiToken::new();
let resolved = resolver
.resolve_or_redacted(&token, &tenant("t1"))
.await
.unwrap();
assert_eq!(resolved, REDACTED_PLACEHOLDER);
}
#[test]
fn category_wire_strings_are_stable() {
for (variant, expected) in [
(DevicePiiCategory::DisplayName, "display_name"),
(DevicePiiCategory::UserAgentString, "user_agent_string"),
(DevicePiiCategory::AcceptLanguage, "accept_language"),
(DevicePiiCategory::IpAddress, "ip_address"),
(DevicePiiCategory::ScreenMetrics, "screen_metrics"),
(DevicePiiCategory::Other, "other"),
] {
assert_eq!(variant.as_str(), expected);
}
}
#[test]
fn pii_token_display_matches_as_str() {
let token = PiiToken::new();
assert_eq!(format!("{token}"), token.as_str());
}
#[tokio::test]
async fn memory_store_len_and_is_empty_track_inserts() {
let store = MemoryDevicePiiStore::new();
assert_eq!(store.len(), 0);
assert!(store.is_empty());
let token = PiiToken::new();
let mapping = DevicePiiMapping {
token: token.clone(),
subject_id: axess_identity::testing::user("u-pii"),
tenant_id: axess_identity::testing::tenant("t-pii"),
category: DevicePiiCategory::IpAddress,
value: "1.2.3.4".to_string(),
created_at: chrono::Utc::now(),
};
store.mappings.insert(token, mapping);
assert_eq!(store.len(), 1);
assert!(!store.is_empty());
}
#[test]
fn parse_refuses_non_canonical_uuid_spellings() {
let u = "6f9619ff-8b86-d011-b42d-00c04fc964ff";
assert!(PiiToken::parse(&format!("pii:{u}")).is_some());
for bad in [
format!("pii:{}", u.to_uppercase()),
format!("pii:{}", u.replace('-', "")),
format!("pii:{{{u}}}"),
format!("pii:urn:uuid:{u}"),
] {
assert!(PiiToken::parse(&bad).is_none(), "{bad} must be refused");
assert!(!PiiToken::is_well_formed(&bad));
}
}
#[tokio::test]
async fn tokens_follow_the_injected_rng() {
use axess_rng::SecureRng;
use axess_rng::testing::MockRng;
let mint = |seed| MemoryDevicePiiStore::new().with_rng(std::sync::Arc::new(MockRng::new(seed)));
let record = |store: MemoryDevicePiiStore| async move {
store
.record(
&user("u"),
&tenant("t"),
DevicePiiCategory::DisplayName,
"v".into(),
Utc::now(),
)
.await
.unwrap()
};
let (a, b, c) = (
record(mint(5)).await,
record(mint(5)).await,
record(mint(6)).await,
);
assert_eq!(a, b);
assert_ne!(a, c);
let mut bytes = [0u8; 16];
MockRng::new(5).fill_bytes(&mut bytes);
let expected = uuid::Builder::from_random_bytes(bytes).into_uuid();
assert_eq!(a, PiiToken::from_uuid(expected));
assert!(PiiToken::is_well_formed(a.as_str()));
}
#[tokio::test]
async fn list_for_subject_is_ordered_by_creation_time() {
use chrono::{Duration, TimeZone};
let store = MemoryDevicePiiStore::new();
let base = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
for i in (0..40i64).rev() {
store
.record(
&user("u"),
&tenant("t"),
DevicePiiCategory::DisplayName,
format!("v{i}"),
base + Duration::minutes(i),
)
.await
.unwrap();
}
let rows = store
.list_for_subject(&user("u"), &tenant("t"))
.await
.unwrap();
let values: Vec<String> = rows.into_iter().map(|m| m.value).collect();
let expected: Vec<String> = (0..40).map(|i| format!("v{i}")).collect();
assert_eq!(values, expected);
}