gatekeep-keepsake 4.0.0

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

use std::sync::{
    Arc, Mutex,
    atomic::{AtomicUsize, Ordering},
};

use std::collections::BTreeMap;

use gatekeep::{
    Context, Fact, FactId, GatekeepError, Locale, StaticFactId, SubjectRef, SubjectSlot, TenantId,
    TrustedServiceBinding,
};
use gatekeep_keepsake::KeepsakeResolver;
use keepsake::{
    ActiveRelation, ActiveRelationSource, ExpiryPolicy, InMemoryActiveRelations, RelationId,
    RelationKey, SubjectRef as KeepsakeSubjectRef, TenantId as KeepsakeTenantId, relation_spec,
};
use thiserror::Error;

pub type TestResult<T> = Result<T, Box<dyn std::error::Error>>;

pub struct PaidPlan;

impl Fact for PaidPlan {
    const ID: StaticFactId = StaticFactId::new("paid_plan");
}

pub struct ResourceMember;

impl Fact for ResourceMember {
    const ID: StaticFactId = StaticFactId::new("resource_member");
}

relation_spec! {
    pub struct PaidPlanRelation {
        id: 0x1111_1111_1111_1111_1111_1111_1111_1111;
        key: ("entitlement", "paid-plan");
        expiry(_at) => ExpiryPolicy::ManualOnly;
    }
}

relation_spec! {
    pub struct ResourceMemberRelation {
        id: 0x2222_2222_2222_2222_2222_2222_2222_2222;
        key: ("membership", "resource-member");
        expiry(_at) => ExpiryPolicy::ManualOnly;
    }
}

relation_spec! {
    pub struct UnboundRelation {
        id: 0x3333_3333_3333_3333_3333_3333_3333_3333;
        key: ("entitlement", "unbound");
        expiry(_at) => ExpiryPolicy::ManualOnly;
    }
}

#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum StoreError {
    #[error("store failed")]
    Failed,

    #[error("source call recorder lock poisoned")]
    Poisoned,
}

#[derive(Clone, Debug, Default)]
pub struct FakeSource {
    inner: InMemoryActiveRelations,
    fail: bool,
    calls: Arc<AtomicUsize>,
    requested_relation_ids: Arc<Mutex<Vec<Vec<RelationId>>>>,
}

impl FakeSource {
    pub fn failing() -> Self {
        Self {
            fail: true,
            ..Self::default()
        }
    }

    pub fn with_active_for_paid_plan(
        self,
        tenant_id: KeepsakeTenantId,
        subject: KeepsakeSubjectRef,
    ) -> TestResult<Self> {
        self.inner.insert_active_for_spec::<PaidPlanRelation>(
            tenant_id,
            0xaaaa_aaaa_aaaa_aaaa_aaaa_aaaa_aaaa_aaaa,
            subject,
            fixed_time(),
        )?;
        Ok(self)
    }

    pub fn with_active_for_resource_member(
        self,
        tenant_id: KeepsakeTenantId,
        subject: KeepsakeSubjectRef,
    ) -> TestResult<Self> {
        self.inner
            .insert_active_for_spec::<ResourceMemberRelation>(
                tenant_id,
                0xbbbb_bbbb_bbbb_bbbb_bbbb_bbbb_bbbb_bbbb,
                subject,
                fixed_time(),
            )?;
        Ok(self)
    }

    pub fn calls(&self) -> usize {
        self.calls.load(Ordering::SeqCst)
    }

    pub fn requested_relation_ids(&self) -> Result<Vec<Vec<RelationId>>, StoreError> {
        self.requested_relation_ids
            .lock()
            .map(|requests| requests.clone())
            .map_err(|_| StoreError::Poisoned)
    }
}

impl ActiveRelationSource for FakeSource {
    type Error = StoreError;

    async fn active_relations_for_subject(
        &self,
        tenant_id: &keepsake::TenantId,
        subject: &KeepsakeSubjectRef,
    ) -> Result<Vec<ActiveRelation>, Self::Error> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        if self.fail {
            return Err(StoreError::Failed);
        }
        self.inner
            .active_relations_for_subject(tenant_id, subject)
            .await
            .map_err(|_| StoreError::Failed)
    }

    async fn active_relations_for_subject_by_ids(
        &self,
        tenant_id: &keepsake::TenantId,
        subject: &KeepsakeSubjectRef,
        relation_ids: &[RelationId],
    ) -> Result<Vec<ActiveRelation>, Self::Error> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        if self.fail {
            return Err(StoreError::Failed);
        }
        self.requested_relation_ids
            .lock()
            .map_err(|_| StoreError::Poisoned)?
            .push(relation_ids.to_vec());
        self.inner
            .active_relations_for_subject_by_ids(tenant_id, subject, relation_ids)
            .await
            .map_err(|_| StoreError::Failed)
    }

    async fn active_relations_for_subject_by_keys(
        &self,
        tenant_id: &keepsake::TenantId,
        subject: &KeepsakeSubjectRef,
        keys: &[RelationKey],
    ) -> Result<Vec<ActiveRelation>, Self::Error> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        if self.fail {
            return Err(StoreError::Failed);
        }
        self.inner
            .active_relations_for_subject_by_keys(tenant_id, subject, keys)
            .await
            .map_err(|_| StoreError::Failed)
    }
}

pub fn resolver_for(principal: &SubjectRef) -> TestResult<KeepsakeResolver<FakeSource>> {
    resolver_for_tenant("tenant_1", principal)
}

pub fn resolver_for_tenant(
    tenant: &str,
    principal: &SubjectRef,
) -> TestResult<KeepsakeResolver<FakeSource>> {
    let subject = tenant_subject(tenant, principal)?;
    resolver_with_subject(KeepsakeTenantId::new(tenant)?, subject)
}

pub fn principal_resolver_for(
    tenant: &str,
    principal: &SubjectRef,
) -> TestResult<KeepsakeResolver<FakeSource>> {
    let subject = KeepsakeSubjectRef::new(principal.kind(), principal.id())?;
    resolver_with_subject(KeepsakeTenantId::new(tenant)?, subject)
}

fn resolver_with_subject(
    tenant_id: KeepsakeTenantId,
    subject: KeepsakeSubjectRef,
) -> TestResult<KeepsakeResolver<FakeSource>> {
    Ok(KeepsakeResolver::new(
        FakeSource::default().with_active_for_paid_plan(tenant_id, subject)?,
    ))
}

pub fn context(tenant: &str, principal: SubjectRef) -> TestResult<Context> {
    let tenant = TenantId::new(tenant)?;
    let binding = TrustedServiceBinding::new(tenant, "gatekeep-keepsake.tests")?;
    Ok(Context::from_trusted_service(
        binding,
        principal,
        Locale::new("en-US")?,
    )?)
}

pub fn context_with_subjects(
    tenant: &str,
    principal: SubjectRef,
    subjects: BTreeMap<SubjectSlot, SubjectRef>,
) -> TestResult<Context> {
    let mut context = context(tenant, principal)?;
    for (slot, subject) in subjects {
        context = context.with_subject(slot, subject);
    }
    Ok(context)
}

pub fn subject(kind: &str, id: &str) -> Result<SubjectRef, GatekeepError> {
    SubjectRef::new(kind, id)
}

pub fn fact_id(value: &str) -> Result<FactId, GatekeepError> {
    FactId::new(value)
}

pub fn tenant_subject(
    tenant: &str,
    principal: &SubjectRef,
) -> Result<KeepsakeSubjectRef, keepsake::KeepsakeError> {
    KeepsakeSubjectRef::new(
        format!(
            "tenant:{}:{}principal:{}:{}",
            tenant.len(),
            tenant,
            principal.kind().len(),
            principal.kind()
        ),
        principal.id(),
    )
}

const fn fixed_time() -> time::OffsetDateTime {
    time::macros::datetime!(2026-01-01 0:00 UTC)
}