gatekeep-keepsake 1.0.1

Keepsake relation fact resolver for gatekeep
Documentation
//! Keepsake resolver integration tests.

mod support;

use std::collections::BTreeMap;

use gatekeep::{FactResolver, Presence, ResolveError, SubjectSlot};
use gatekeep_keepsake::{
    FactBinding, KeepsakeResolveError, KeepsakeResolver, KeepsakeTargetError,
    PrincipalSubjectMapper, QueryPresence, tenant_scoped_subject,
};
use keepsake::{ActorRef, CommandContext, RelationSpec};
use support::{
    PaidPlan, PaidPlanRelation, ResourceMember, ResourceMemberRelation, StoreError, TestResult,
    UnboundRelation, context, context_with_subjects, fact_id, principal_resolver_for, resolver_for,
    resolver_for_tenant, subject,
};

#[tokio::test]
async fn decision_resolution_maps_active_relations_to_known_facts() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let resolver = resolver_for(&principal)?
        .with_relation_spec::<PaidPlan, PaidPlanRelation>()?
        .with_relation_spec::<ResourceMember, ResourceMemberRelation>()?;

    let facts = resolver
        .resolve_for_decision(
            &[fact_id("paid_plan")?, fact_id("resource_member")?],
            &context("tenant_1", principal)?,
        )
        .await?;

    assert_eq!(facts.presence(&fact_id("paid_plan")?), Presence::Present);
    assert_eq!(
        facts.presence(&fact_id("resource_member")?),
        Presence::Absent
    );
    assert_eq!(
        resolver.source().requested_relation_ids()?,
        vec![vec![PaidPlanRelation::ID, ResourceMemberRelation::ID]]
    );
    Ok(())
}

#[tokio::test]
async fn target_for_fact_matches_decision_lookup_target() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let resolver = resolver_for(&principal)?
        .with_relation_spec::<PaidPlan, PaidPlanRelation>()?
        .with_relation_spec::<ResourceMember, ResourceMemberRelation>()?;
    let cx = context("tenant_1", principal)?;

    let target = resolver.target_for_fact(&fact_id("paid_plan")?, &cx)?;
    let facts = resolver
        .resolve_for_decision(&[fact_id("paid_plan")?], &cx)
        .await?;

    assert_eq!(facts.presence(&fact_id("paid_plan")?), Presence::Present);
    assert_eq!(target.fact, fact_id("paid_plan")?);
    assert_eq!(
        target.subject,
        support::tenant_subject("tenant_1", &cx.principal)?
    );
    assert_eq!(target.relation_id, PaidPlanRelation::ID);
    assert_eq!(target.subject_slot, None);
    assert_eq!(
        resolver.source().requested_relation_ids()?,
        vec![vec![PaidPlanRelation::ID]]
    );
    Ok(())
}

#[tokio::test]
async fn decision_resolution_can_bind_distinct_subject_slots() -> TestResult<()> {
    let skill_slot = SubjectSlot::new("skill-version")?;
    let source_slot = SubjectSlot::new("purlieu-source")?;
    let skill = subject("skill-version", "std/core@0.1.0")?;
    let source = subject("purlieu-source", "external/std")?;
    let resolver = KeepsakeResolver::new(
        support::FakeSource::default()
            .with_active_for_paid_plan(keepsake::SubjectRef::new(skill.kind(), skill.id())?)?
            .with_active_for_resource_member(keepsake::SubjectRef::new(
                source.kind(),
                source.id(),
            )?)?,
    )
    .with_binding(FactBinding::for_relation_spec_on_subject::<
        PaidPlan,
        PaidPlanRelation,
    >(skill_slot.clone())?)
    .with_binding(FactBinding::for_relation_spec_on_subject::<
        ResourceMember,
        ResourceMemberRelation,
    >(source_slot.clone())?);
    let cx = context_with_subjects(
        "tenant_1",
        subject("user", "u_1")?,
        BTreeMap::from([(skill_slot, skill), (source_slot, source)]),
    )?;

    let facts = resolver
        .resolve_for_decision(&[fact_id("paid_plan")?, fact_id("resource_member")?], &cx)
        .await?;

    assert_eq!(facts.presence(&fact_id("paid_plan")?), Presence::Present);
    assert_eq!(
        facts.presence(&fact_id("resource_member")?),
        Presence::Present
    );
    let mut requested = resolver.source().requested_relation_ids()?;
    requested.sort();
    assert_eq!(
        requested,
        vec![vec![PaidPlanRelation::ID], vec![ResourceMemberRelation::ID]]
    );
    Ok(())
}

#[test]
fn target_for_fact_resolves_request_scoped_subject_slot() -> TestResult<()> {
    let resource_slot = SubjectSlot::new("resource")?;
    let resource = subject("resource", "case_1")?;
    let resolver = resolver_for(&subject("user", "u_1")?)?.with_binding(
        FactBinding::for_relation_spec_on_subject::<ResourceMember, ResourceMemberRelation>(
            resource_slot.clone(),
        )?,
    );
    let cx = context_with_subjects(
        "tenant_1",
        subject("user", "u_1")?,
        BTreeMap::from([(resource_slot.clone(), resource)]),
    )?;

    let target = resolver.target_for_fact(&fact_id("resource_member")?, &cx)?;

    assert_eq!(target.fact, fact_id("resource_member")?);
    assert_eq!(
        target.subject,
        keepsake::SubjectRef::new("resource", "case_1")?
    );
    assert_eq!(target.relation_id, ResourceMemberRelation::ID);
    assert_eq!(target.subject_slot, Some(resource_slot));
    Ok(())
}

#[tokio::test]
async fn tenant_scoped_mapping_keeps_equal_principals_separate() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let resolver = resolver_for(&principal)?
        .with_relation_spec::<PaidPlan, PaidPlanRelation>()?
        .with_relation_spec::<ResourceMember, ResourceMemberRelation>()?;

    let tenant_two = resolver
        .resolve_for_decision(&[fact_id("paid_plan")?], &context("tenant_2", principal)?)
        .await?;

    assert_eq!(
        tenant_two.presence(&fact_id("paid_plan")?),
        Presence::Absent
    );
    Ok(())
}

#[tokio::test]
async fn tenant_scoped_mapping_does_not_collide_on_colons() -> TestResult<()> {
    let seeded_principal = subject("c", "u_1")?;
    let resolver = resolver_for_tenant("a:b", &seeded_principal)?
        .with_relation_spec::<PaidPlan, PaidPlanRelation>()?;

    let colliding_join_shape = resolver
        .resolve_for_decision(
            &[fact_id("paid_plan")?],
            &context("a", subject("b:c", "u_1")?)?,
        )
        .await?;

    assert_eq!(
        colliding_join_shape.presence(&fact_id("paid_plan")?),
        Presence::Absent
    );
    Ok(())
}

#[tokio::test]
async fn principal_only_mapping_is_available_for_existing_subject_schemes() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let resolver = principal_resolver_for(&principal)?
        .map_subjects(PrincipalSubjectMapper)
        .with_relation_spec::<PaidPlan, PaidPlanRelation>()?;

    let facts = resolver
        .resolve_for_decision(&[fact_id("paid_plan")?], &context("tenant_2", principal)?)
        .await?;

    assert_eq!(facts.presence(&fact_id("paid_plan")?), Presence::Present);
    Ok(())
}

#[tokio::test]
async fn query_resolution_can_mix_known_and_deferred_facts() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let resolver = resolver_for(&principal)?
        .with_relation_spec::<PaidPlan, PaidPlanRelation>()?
        .with_relation_spec_query_presence::<ResourceMember, ResourceMemberRelation>(
            QueryPresence::Defer,
        )?;

    let facts = resolver
        .resolve_for_query(
            &[fact_id("paid_plan")?, fact_id("resource_member")?],
            &context("tenant_1", principal)?,
        )
        .await?;

    assert_eq!(facts.presence(&fact_id("paid_plan")?), Presence::Present);
    assert_eq!(
        facts.presence(&fact_id("resource_member")?),
        Presence::Unknown
    );
    assert_eq!(
        resolver.source().requested_relation_ids()?,
        vec![vec![PaidPlanRelation::ID]]
    );
    Ok(())
}

#[tokio::test]
async fn missing_fact_reports_resolve_error_without_source_lookup() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let resolver = resolver_for(&principal)?;

    let result = resolver
        .resolve_for_decision(&[fact_id("paid_plan")?], &context("tenant_1", principal)?)
        .await;

    assert!(matches!(
        result,
        Err(ResolveError::MissingFact(fact)) if fact.as_str() == "paid_plan"
    ));
    assert_eq!(resolver.source().calls(), 0);
    Ok(())
}

#[test]
fn target_for_fact_reports_missing_binding() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let resolver = resolver_for(&principal)?;
    let fact = fact_id("paid_plan")?;

    let result = resolver.target_for_fact(&fact, &context("tenant_1", principal)?);

    assert!(matches!(
        result,
        Err(KeepsakeTargetError::MissingBinding { fact: missing }) if missing == fact
    ));
    Ok(())
}

#[tokio::test]
async fn missing_subject_slot_reports_context_error_without_source_lookup() -> TestResult<()> {
    let slot = SubjectSlot::new("resource")?;
    let principal = subject("user", "u_1")?;
    let resolver =
        resolver_for(&principal)?.with_binding(FactBinding::for_relation_spec_on_subject::<
            PaidPlan,
            PaidPlanRelation,
        >(slot.clone())?);

    let result = resolver
        .resolve_for_decision(&[fact_id("paid_plan")?], &context("tenant_1", principal)?)
        .await;

    assert!(matches!(
        result,
        Err(ResolveError::MissingSubject { fact, slot: missing })
            if fact.as_str() == "paid_plan" && missing == slot
    ));
    assert_eq!(resolver.source().calls(), 0);
    Ok(())
}

#[test]
fn target_for_fact_reports_missing_subject_slot() -> TestResult<()> {
    let slot = SubjectSlot::new("resource")?;
    let principal = subject("user", "u_1")?;
    let resolver =
        resolver_for(&principal)?.with_binding(FactBinding::for_relation_spec_on_subject::<
            PaidPlan,
            PaidPlanRelation,
        >(slot.clone())?);
    let fact = fact_id("paid_plan")?;

    let result = resolver.target_for_fact(&fact, &context("tenant_1", principal)?);

    assert!(matches!(
        result,
        Err(KeepsakeTargetError::MissingSubjectSlot { fact: missing, slot: missing_slot })
            if missing == fact && missing_slot == slot
    ));
    assert_eq!(resolver.source().calls(), 0);
    Ok(())
}

#[test]
fn revoke_by_subject_preserves_target_subject_and_relation() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let resolver = resolver_for(&principal)?.with_relation_spec::<PaidPlan, PaidPlanRelation>()?;
    let cx = context("tenant_1", principal)?;
    let target = resolver.target_for_fact(&fact_id("paid_plan")?, &cx)?;
    let at =
        chrono::DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")?.with_timezone(&chrono::Utc);
    let command_context = CommandContext::new(ActorRef::new("system", "test")?);

    let command = target.revoke_by_subject(at, command_context.clone());

    assert_eq!(command.subject, target.subject);
    assert_eq!(command.relation_id, target.relation_id);
    assert_eq!(command.at, at);
    assert_eq!(command.context, command_context);
    Ok(())
}

#[tokio::test]
async fn source_errors_are_preserved_as_backend_errors() -> TestResult<()> {
    let resolver = KeepsakeResolver::new(support::FakeSource::failing())
        .with_binding(FactBinding::for_relation_spec::<PaidPlan, PaidPlanRelation>()?);

    let result = resolver
        .resolve_for_decision(
            &[fact_id("paid_plan")?],
            &context("tenant_1", subject("user", "u_1")?)?,
        )
        .await;

    assert!(matches!(
        result,
        Err(ResolveError::Backend(KeepsakeResolveError::Source(
            StoreError::Failed
        )))
    ));
    Ok(())
}

#[test]
fn manual_bindings_can_defer_runtime_fact_ids() -> TestResult<()> {
    let binding = FactBinding::with_query_presence(
        fact_id("runtime_fact")?,
        UnboundRelation::ID,
        QueryPresence::Defer,
    );

    assert_eq!(binding.fact().as_str(), "runtime_fact");
    assert_eq!(binding.relation_id(), UnboundRelation::ID);
    assert_eq!(binding.query_presence(), QueryPresence::Defer);
    Ok(())
}

#[test]
fn typed_binding_aliases_express_query_behavior() -> TestResult<()> {
    let resolved = FactBinding::resolve_relation::<PaidPlan, PaidPlanRelation>()?;
    let deferred = FactBinding::defer_relation::<ResourceMember, ResourceMemberRelation>()?;

    assert_eq!(resolved.query_presence(), QueryPresence::Resolve);
    assert_eq!(deferred.query_presence(), QueryPresence::Defer);
    Ok(())
}

#[test]
fn tenant_scoped_subject_helper_matches_default_mapper() -> TestResult<()> {
    let principal = subject("user", "u_1")?;
    let context = context("tenant_1", principal)?;

    assert_eq!(
        tenant_scoped_subject(&context)?,
        support::tenant_subject("tenant_1", &subject("user", "u_1")?)?
    );
    Ok(())
}