keepsake 0.5.0

Deterministic lifecycle model for auditable relation assignments
Documentation
use std::collections::BTreeMap;

use chrono::{DateTime, Utc};
use uuid::Uuid;

use crate::ExpiryPolicy;

use super::*;

type TestResult<T> = core::result::Result<T, TestError>;
type TimestampResult<T> = core::result::Result<T, chrono::ParseError>;

#[derive(Debug, thiserror::Error)]
enum TestError {
    #[error(transparent)]
    Chrono(#[from] chrono::ParseError),

    #[error(transparent)]
    Keepsake(#[from] KeepsakeError),

    #[error(transparent)]
    SerdeJson(#[from] serde_json::Error),
}

fn ts(value: &str) -> TimestampResult<DateTime<Utc>> {
    DateTime::parse_from_rfc3339(value).map(|timestamp| timestamp.with_timezone(&Utc))
}

fn record(expiry: ExpiryPolicy, state: LifecycleState) -> TestResult<KeepsakeRecord> {
    Ok(KeepsakeRecord {
        id: Uuid::nil(),
        subject: SubjectRef::new("user", "u_1")?,
        relation_id: Uuid::from_u128(1),
        state,
        expires_at: expiry.timed_expiry(),
        expiry,
        applied_at: ts("2026-01-01T00:00:00Z")?,
        fulfilled_at: None,
        revoked_at: None,
        metadata: BTreeMap::new(),
    })
}

#[test]
fn relation_definition_enabled_and_disabled_helpers_set_state() -> TestResult<()> {
    let key = RelationKey::new("tag", "trusted")?;
    let enabled = RelationDefinition::enabled(Uuid::nil(), key.clone(), ExpiryPolicy::ManualOnly)?;
    let disabled = RelationDefinition::disabled(Uuid::nil(), key, ExpiryPolicy::ManualOnly)?;

    assert!(enabled.enabled);
    assert!(!disabled.enabled);
    Ok(())
}

#[test]
fn relation_key_components_validate_independently() {
    assert_eq!(
        RelationKind::new(" ").map_err(|error| error.to_string()),
        Err("relation.kind must not be empty".to_owned())
    );
    assert_eq!(
        RelationName::new(" ").map_err(|error| error.to_string()),
        Err("relation.name must not be empty".to_owned())
    );
}

#[test]
fn relation_key_components_format_for_logs_and_labels() -> TestResult<()> {
    let kind = RelationKind::new("sanction")?;
    let name = RelationName::new("mute_24h")?;

    assert_eq!(kind.as_ref(), "sanction");
    assert_eq!(kind.to_string(), "sanction");
    assert_eq!(name.as_ref(), "mute_24h");
    assert_eq!(name.to_string(), "mute_24h");
    Ok(())
}

crate::relation_spec! {
    struct TrustedTag {
        id: 0;
        key: ("tag", "trusted");
        expiry(_at) => ExpiryPolicy::ManualOnly;
    }
}

crate::relation_spec! {
    struct DisabledTimedSanction {
        id: 0x018f_0000_0000_7000_8000_0000_0000_0003;
        key: ("sanction", "review_hold");
        enabled: false;
        expiry(at) => ExpiryPolicy::At { timestamp: at };
    }
}

#[test]
fn relation_definition_can_be_built_from_spec() -> TestResult<()> {
    let definition = RelationDefinition::from_spec::<TrustedTag>(
        DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
            .map(|timestamp| timestamp.with_timezone(&Utc))?,
    )?;

    assert_eq!(definition.id, Uuid::nil());
    assert_eq!(definition.key.kind(), "tag");
    assert_eq!(definition.key.name(), "trusted");
    assert!(definition.enabled);
    assert_eq!(definition.expiry, ExpiryPolicy::ManualOnly);
    Ok(())
}

#[test]
fn relation_spec_macro_supports_disabled_timed_specs() -> TestResult<()> {
    let at = DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
        .map(|timestamp| timestamp.with_timezone(&Utc))?;
    let definition = RelationDefinition::from_spec::<DisabledTimedSanction>(at)?;

    assert_eq!(
        definition.id,
        Uuid::from_u128(0x018f_0000_0000_7000_8000_0000_0000_0003)
    );
    assert_eq!(definition.key.kind(), "sanction");
    assert_eq!(definition.key.name(), "review_hold");
    assert!(!definition.enabled);
    assert_eq!(definition.expiry, ExpiryPolicy::At { timestamp: at });
    Ok(())
}

#[test]
fn applied_keepsake_exposes_common_accessors() -> TestResult<()> {
    let relation = RelationDefinition::enabled(
        Uuid::from_u128(1),
        RelationKey::new("tag", "trusted")?,
        ExpiryPolicy::At {
            timestamp: ts("2026-01-02T00:00:00Z")?,
        },
    )?;
    let keepsake = Keepsake::applied(
        Uuid::from_u128(2),
        SubjectRef::new("user", "u_1")?,
        &relation,
        ts("2026-01-01T00:00:00Z")?,
        BTreeMap::from([("source".to_owned(), "test".to_owned())]),
    )?;

    assert_eq!(keepsake.id(), Uuid::from_u128(2));
    assert_eq!(keepsake.subject().id, "u_1");
    assert_eq!(keepsake.relation_id(), relation.id);
    assert_eq!(keepsake.state(), LifecycleState::Applied);
    assert!(matches!(keepsake.lifecycle(), KeepsakeLifecycle::Applied));
    assert!(keepsake.is_active());
    assert!(!keepsake.is_revoked());
    assert!(!keepsake.is_expired());
    assert_eq!(keepsake.expires_at(), Some(ts("2026-01-02T00:00:00Z")?));
    assert_eq!(keepsake.ended_at(), None);
    assert_eq!(keepsake.revoked_at(), None);
    assert_eq!(keepsake.expired_at(), None);
    assert_eq!(keepsake.fulfilled_at(), None);
    assert_eq!(
        keepsake.metadata().get("source").map(String::as_str),
        Some("test")
    );
    Ok(())
}

#[test]
fn active_relation_validates_relation_match_and_active_state() -> TestResult<()> {
    let relation = RelationDefinition::enabled(
        Uuid::from_u128(1),
        RelationKey::new("tag", "trusted")?,
        ExpiryPolicy::ManualOnly,
    )?;
    let keepsake = Keepsake::applied(
        Uuid::from_u128(2),
        SubjectRef::new("user", "u_1")?,
        &relation,
        ts("2026-01-01T00:00:00Z")?,
        BTreeMap::new(),
    )?;

    let active = ActiveRelation::new(keepsake.clone(), relation.clone())?;
    assert_eq!(active.keepsake().id(), keepsake.id());
    assert_eq!(active.relation(), &relation);

    let wrong_relation = RelationDefinition::enabled(
        Uuid::from_u128(3),
        RelationKey::new("tag", "admin")?,
        ExpiryPolicy::ManualOnly,
    )?;
    assert!(matches!(
        ActiveRelation::new(keepsake, wrong_relation),
        Err(KeepsakeError::ActiveRelationMismatch { .. })
    ));

    let mut revoked = record(ExpiryPolicy::ManualOnly, LifecycleState::Revoked)?;
    revoked.revoked_at = Some(ts("2026-01-02T00:00:00Z")?);
    let revoked = Keepsake::try_from(revoked)?;
    assert!(matches!(
        ActiveRelation::new(revoked, relation),
        Err(KeepsakeError::InactiveActiveRelation { .. })
    ));
    Ok(())
}

#[test]
fn valid_flat_records_convert_to_typed_lifecycles() -> TestResult<()> {
    let mut revoked = record(ExpiryPolicy::ManualOnly, LifecycleState::Revoked)?;
    revoked.revoked_at = Some(ts("2026-01-03T00:00:00Z")?);
    let revoked = Keepsake::try_from(revoked)?;
    assert!(revoked.is_revoked());
    assert_eq!(revoked.ended_at(), Some(ts("2026-01-03T00:00:00Z")?));

    let timed_expiry = Keepsake::try_from(record(
        ExpiryPolicy::At {
            timestamp: ts("2026-01-02T00:00:00Z")?,
        },
        LifecycleState::Expired,
    )?)?;
    assert_eq!(timed_expiry.state(), LifecycleState::Expired);
    assert_eq!(timed_expiry.expired_at(), Some(ts("2026-01-02T00:00:00Z")?));
    assert_eq!(timed_expiry.fulfilled_at(), None);

    let mut fulfilled = record(
        ExpiryPolicy::WhenFulfilled {
            policy: crate::policy::FulfillmentPolicy::CounterAtLeast {
                key: "messages_sent".to_owned(),
                threshold: 3,
            },
        },
        LifecycleState::Expired,
    )?;
    fulfilled.fulfilled_at = Some(ts("2026-01-04T00:00:00Z")?);
    let fulfilled = Keepsake::try_from(fulfilled)?;
    assert_eq!(fulfilled.expired_at(), Some(ts("2026-01-04T00:00:00Z")?));
    assert_eq!(fulfilled.fulfilled_at(), Some(ts("2026-01-04T00:00:00Z")?));
    Ok(())
}

#[test]
fn invalid_flat_records_are_rejected() -> TestResult<()> {
    let mut manual_expired = record(ExpiryPolicy::ManualOnly, LifecycleState::Expired)?;
    assert!(Keepsake::try_from(manual_expired.clone()).is_err());

    let mut revoked_with_fulfilled = record(ExpiryPolicy::ManualOnly, LifecycleState::Revoked)?;
    revoked_with_fulfilled.revoked_at = Some(ts("2026-01-03T00:00:00Z")?);
    revoked_with_fulfilled.fulfilled_at = Some(ts("2026-01-03T00:00:00Z")?);
    assert!(Keepsake::try_from(revoked_with_fulfilled).is_err());

    let mut applied_with_terminal = record(ExpiryPolicy::ManualOnly, LifecycleState::Applied)?;
    applied_with_terminal.revoked_at = Some(ts("2026-01-03T00:00:00Z")?);
    assert!(Keepsake::try_from(applied_with_terminal).is_err());

    let mut fulfilled_with_expires = record(
        ExpiryPolicy::WhenFulfilled {
            policy: crate::policy::FulfillmentPolicy::CounterAtLeast {
                key: "messages_sent".to_owned(),
                threshold: 3,
            },
        },
        LifecycleState::Expired,
    )?;
    fulfilled_with_expires.expires_at = Some(ts("2026-01-02T00:00:00Z")?);
    fulfilled_with_expires.fulfilled_at = Some(ts("2026-01-03T00:00:00Z")?);
    assert!(Keepsake::try_from(fulfilled_with_expires).is_err());

    manual_expired.state = LifecycleState::Revoked;
    assert!(Keepsake::try_from(manual_expired).is_err());
    Ok(())
}

#[test]
fn serde_uses_flat_record_shape_and_validates_on_read() -> TestResult<()> {
    let valid = record(ExpiryPolicy::ManualOnly, LifecycleState::Applied)?;
    let json = serde_json::to_string(&valid)?;
    let keepsake = serde_json::from_str::<Keepsake>(&json)?;
    let round_trip = serde_json::to_value(&keepsake)?;

    assert_eq!(keepsake.state(), LifecycleState::Applied);
    assert_eq!(round_trip, serde_json::to_value(&valid)?);

    let mut invalid = valid;
    invalid.revoked_at = Some(ts("2026-01-03T00:00:00Z")?);
    let json = serde_json::to_string(&invalid)?;
    assert!(serde_json::from_str::<Keepsake>(&json).is_err());
    Ok(())
}