use async_trait::async_trait;
use modkit_security::SecurityContext;
use tenant_resolver_sdk::{
GetAncestorsOptions, GetAncestorsResponse, GetDescendantsOptions, GetDescendantsResponse,
GetTenantsOptions, IsAncestorOptions, TenantId, TenantInfo, TenantRef, TenantResolverError,
TenantResolverPluginClient, TenantStatus, matches_status,
};
use super::service::Service;
const TENANT_NAME: &str = "Default";
fn build_tenant_info(id: TenantId) -> TenantInfo {
TenantInfo {
id,
name: TENANT_NAME.to_owned(),
status: TenantStatus::Active,
tenant_type: None,
parent_id: None, self_managed: false, }
}
fn build_tenant_ref(id: TenantId) -> TenantRef {
TenantRef {
id,
status: TenantStatus::Active,
tenant_type: None,
parent_id: None, self_managed: false, }
}
#[async_trait]
impl TenantResolverPluginClient for Service {
async fn get_tenant(
&self,
ctx: &SecurityContext,
id: TenantId,
) -> Result<TenantInfo, TenantResolverError> {
let ctx_tenant = TenantId(ctx.subject_tenant_id());
if ctx_tenant.is_nil() {
return Err(TenantResolverError::TenantNotFound { tenant_id: id });
}
if id == ctx_tenant {
Ok(build_tenant_info(id))
} else {
Err(TenantResolverError::TenantNotFound { tenant_id: id })
}
}
async fn get_tenants(
&self,
ctx: &SecurityContext,
ids: &[TenantId],
options: &GetTenantsOptions,
) -> Result<Vec<TenantInfo>, TenantResolverError> {
let ctx_tenant = TenantId(ctx.subject_tenant_id());
if ctx_tenant.is_nil() {
return Ok(vec![]);
}
let mut result = Vec::new();
let mut seen = std::collections::HashSet::new();
for id in ids {
if !seen.insert(id) {
continue; }
if *id == ctx_tenant {
let tenant = build_tenant_info(*id);
if matches_status(&tenant, &options.status) {
result.push(tenant);
}
}
}
Ok(result)
}
async fn get_ancestors(
&self,
ctx: &SecurityContext,
id: TenantId,
_options: &GetAncestorsOptions,
) -> Result<GetAncestorsResponse, TenantResolverError> {
let ctx_tenant = TenantId(ctx.subject_tenant_id());
if ctx_tenant.is_nil() {
return Err(TenantResolverError::TenantNotFound { tenant_id: id });
}
if id != ctx_tenant {
return Err(TenantResolverError::TenantNotFound { tenant_id: id });
}
Ok(GetAncestorsResponse {
tenant: build_tenant_ref(id),
ancestors: vec![], })
}
async fn get_descendants(
&self,
ctx: &SecurityContext,
id: TenantId,
_options: &GetDescendantsOptions,
) -> Result<GetDescendantsResponse, TenantResolverError> {
let ctx_tenant = TenantId(ctx.subject_tenant_id());
if ctx_tenant.is_nil() {
return Err(TenantResolverError::TenantNotFound { tenant_id: id });
}
if id != ctx_tenant {
return Err(TenantResolverError::TenantNotFound { tenant_id: id });
}
Ok(GetDescendantsResponse {
tenant: build_tenant_ref(id),
descendants: vec![],
})
}
async fn is_ancestor(
&self,
ctx: &SecurityContext,
ancestor_id: TenantId,
descendant_id: TenantId,
_options: &IsAncestorOptions,
) -> Result<bool, TenantResolverError> {
let ctx_tenant = TenantId(ctx.subject_tenant_id());
if ctx_tenant.is_nil() {
return Err(TenantResolverError::TenantNotFound {
tenant_id: ancestor_id,
});
}
if ancestor_id != ctx_tenant {
return Err(TenantResolverError::TenantNotFound {
tenant_id: ancestor_id,
});
}
if descendant_id != ctx_tenant {
return Err(TenantResolverError::TenantNotFound {
tenant_id: descendant_id,
});
}
Ok(false)
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use tenant_resolver_sdk::TenantStatus;
use uuid::Uuid;
fn ctx_for_tenant(tenant_id: TenantId) -> SecurityContext {
SecurityContext::builder()
.subject_id(Uuid::new_v4())
.subject_tenant_id(tenant_id.0)
.build()
.unwrap()
}
const TENANT_A: &str = "11111111-1111-1111-1111-111111111111";
const TENANT_B: &str = "22222222-2222-2222-2222-222222222222";
#[tokio::test]
async fn get_tenant_returns_info_for_matching_id() {
let service = Service;
let tenant_id = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let ctx = ctx_for_tenant(tenant_id);
let result = service.get_tenant(&ctx, tenant_id).await;
assert!(result.is_ok());
let info = result.unwrap();
assert_eq!(info.id, tenant_id);
assert_eq!(info.name, TENANT_NAME);
assert_eq!(info.status, TenantStatus::Active);
assert!(info.tenant_type.is_none());
assert!(info.parent_id.is_none());
assert!(!info.self_managed);
}
#[tokio::test]
async fn get_tenant_returns_error_for_different_id() {
let service = Service;
let ctx_tenant = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let query_tenant = TenantId(Uuid::parse_str(TENANT_B).unwrap());
let ctx = ctx_for_tenant(ctx_tenant);
let result = service.get_tenant(&ctx, query_tenant).await;
assert!(result.is_err());
match result.unwrap_err() {
TenantResolverError::TenantNotFound { tenant_id } => {
assert_eq!(tenant_id, query_tenant);
}
other => panic!("Expected TenantNotFound, got: {other:?}"),
}
}
#[tokio::test]
async fn get_tenant_rejects_nil_uuid() {
let service = Service;
let nil_id = TenantId::nil();
let ctx = ctx_for_tenant(nil_id);
let result = service.get_tenant(&ctx, nil_id).await;
assert!(result.is_err());
match result.unwrap_err() {
TenantResolverError::TenantNotFound { tenant_id } => {
assert_eq!(tenant_id, nil_id);
}
other => panic!("Expected TenantNotFound, got: {other:?}"),
}
}
#[tokio::test]
async fn get_tenants_rejects_nil_uuid() {
let service = Service;
let nil_id = TenantId::nil();
let ctx = ctx_for_tenant(nil_id);
let result = service
.get_tenants(&ctx, &[nil_id], &GetTenantsOptions::default())
.await;
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[tokio::test]
async fn get_ancestors_rejects_nil_uuid() {
let service = Service;
let nil_id = TenantId::nil();
let ctx = ctx_for_tenant(nil_id);
let result = service
.get_ancestors(&ctx, nil_id, &GetAncestorsOptions::default())
.await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TenantResolverError::TenantNotFound { .. }
));
}
#[tokio::test]
async fn get_descendants_rejects_nil_uuid() {
let service = Service;
let nil_id = TenantId::nil();
let ctx = ctx_for_tenant(nil_id);
let result = service
.get_descendants(&ctx, nil_id, &GetDescendantsOptions::default())
.await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TenantResolverError::TenantNotFound { .. }
));
}
#[tokio::test]
async fn is_ancestor_rejects_nil_uuid() {
let service = Service;
let nil_id = TenantId::nil();
let ctx = ctx_for_tenant(nil_id);
let result = service
.is_ancestor(&ctx, nil_id, nil_id, &IsAncestorOptions::default())
.await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TenantResolverError::TenantNotFound { .. }
));
}
#[tokio::test]
async fn get_tenants_returns_self() {
let service = Service;
let tenant_id = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let ctx = ctx_for_tenant(tenant_id);
let result = service
.get_tenants(&ctx, &[tenant_id], &GetTenantsOptions::default())
.await;
assert!(result.is_ok());
let tenants = result.unwrap();
assert_eq!(tenants.len(), 1);
assert_eq!(tenants[0].id, tenant_id);
}
#[tokio::test]
async fn get_tenants_skips_nonexistent() {
let service = Service;
let ctx_tenant = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let other_tenant = TenantId(Uuid::parse_str(TENANT_B).unwrap());
let ctx = ctx_for_tenant(ctx_tenant);
let result = service
.get_tenants(
&ctx,
&[ctx_tenant, other_tenant],
&GetTenantsOptions::default(),
)
.await;
assert!(result.is_ok());
let tenants = result.unwrap();
assert_eq!(tenants.len(), 1);
assert_eq!(tenants[0].id, ctx_tenant);
}
#[tokio::test]
async fn get_tenants_with_filter() {
let service = Service;
let tenant_id = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let ctx = ctx_for_tenant(tenant_id);
let opts = GetTenantsOptions {
status: vec![TenantStatus::Suspended],
};
let result = service.get_tenants(&ctx, &[tenant_id], &opts).await;
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[tokio::test]
async fn get_ancestors_returns_empty_for_self() {
let service = Service;
let tenant_id = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let ctx = ctx_for_tenant(tenant_id);
let result = service
.get_ancestors(&ctx, tenant_id, &GetAncestorsOptions::default())
.await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.tenant.id, tenant_id);
assert!(response.ancestors.is_empty());
}
#[tokio::test]
async fn get_ancestors_error_for_different_id() {
let service = Service;
let ctx_tenant = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let other_tenant = TenantId(Uuid::parse_str(TENANT_B).unwrap());
let ctx = ctx_for_tenant(ctx_tenant);
let result = service
.get_ancestors(&ctx, other_tenant, &GetAncestorsOptions::default())
.await;
assert!(result.is_err());
match result.unwrap_err() {
TenantResolverError::TenantNotFound { tenant_id } => {
assert_eq!(tenant_id, other_tenant);
}
other => panic!("Expected TenantNotFound, got: {other:?}"),
}
}
#[tokio::test]
async fn get_descendants_returns_empty_for_self() {
let service = Service;
let tenant_id = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let ctx = ctx_for_tenant(tenant_id);
let result = service
.get_descendants(&ctx, tenant_id, &GetDescendantsOptions::default())
.await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.tenant.id, tenant_id);
assert!(response.descendants.is_empty());
}
#[tokio::test]
async fn get_descendants_error_for_different_id() {
let service = Service;
let ctx_tenant = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let other_tenant = TenantId(Uuid::parse_str(TENANT_B).unwrap());
let ctx = ctx_for_tenant(ctx_tenant);
let result = service
.get_descendants(&ctx, other_tenant, &GetDescendantsOptions::default())
.await;
assert!(result.is_err());
match result.unwrap_err() {
TenantResolverError::TenantNotFound { tenant_id } => {
assert_eq!(tenant_id, other_tenant);
}
other => panic!("Expected TenantNotFound, got: {other:?}"),
}
}
#[tokio::test]
async fn is_ancestor_self_returns_false() {
let service = Service;
let tenant_id = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let ctx = ctx_for_tenant(tenant_id);
let result = service
.is_ancestor(&ctx, tenant_id, tenant_id, &IsAncestorOptions::default())
.await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn is_ancestor_error_for_different_ancestor() {
let service = Service;
let ctx_tenant = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let other_tenant = TenantId(Uuid::parse_str(TENANT_B).unwrap());
let ctx = ctx_for_tenant(ctx_tenant);
let result = service
.is_ancestor(
&ctx,
other_tenant,
ctx_tenant,
&IsAncestorOptions::default(),
)
.await;
assert!(result.is_err());
match result.unwrap_err() {
TenantResolverError::TenantNotFound { tenant_id } => {
assert_eq!(tenant_id, other_tenant);
}
other => panic!("Expected TenantNotFound, got: {other:?}"),
}
}
#[tokio::test]
async fn is_ancestor_error_for_different_descendant() {
let service = Service;
let ctx_tenant = TenantId(Uuid::parse_str(TENANT_A).unwrap());
let other_tenant = TenantId(Uuid::parse_str(TENANT_B).unwrap());
let ctx = ctx_for_tenant(ctx_tenant);
let result = service
.is_ancestor(
&ctx,
ctx_tenant,
other_tenant,
&IsAncestorOptions::default(),
)
.await;
assert!(result.is_err());
match result.unwrap_err() {
TenantResolverError::TenantNotFound { tenant_id } => {
assert_eq!(tenant_id, other_tenant);
}
other => panic!("Expected TenantNotFound, got: {other:?}"),
}
}
}