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());
}