1use std::collections::{HashMap, HashSet};
4
5use modkit_macros::domain_model;
6use tenant_resolver_sdk::{
7 BarrierMode, TenantId, TenantInfo, TenantRef, TenantResolverError, TenantStatus, matches_status,
8};
9
10use crate::config::StaticTrPluginConfig;
11
12#[domain_model]
17pub struct Service {
18 pub(super) tenants: HashMap<TenantId, TenantInfo>,
20
21 pub(super) children: HashMap<TenantId, Vec<TenantId>>,
23}
24
25impl Service {
26 #[must_use]
28 pub fn from_config(cfg: &StaticTrPluginConfig) -> Self {
29 let tenants: HashMap<TenantId, TenantInfo> = cfg
30 .tenants
31 .iter()
32 .map(|t| {
33 (
34 t.id,
35 TenantInfo {
36 id: t.id,
37 name: t.name.clone(),
38 status: t.status,
39 tenant_type: t.tenant_type.clone(),
40 parent_id: t.parent_id,
41 self_managed: t.self_managed,
42 },
43 )
44 })
45 .collect();
46
47 let mut children: HashMap<TenantId, Vec<TenantId>> = HashMap::new();
49 for tenant in tenants.values() {
50 if let Some(parent_id) = tenant.parent_id {
51 children.entry(parent_id).or_default().push(tenant.id);
52 }
53 }
54
55 Self { tenants, children }
56 }
57
58 pub(super) fn matches_status_filter(tenant: &TenantInfo, statuses: &[TenantStatus]) -> bool {
60 matches_status(tenant, statuses)
61 }
62
63 pub(super) fn collect_ancestors(
72 &self,
73 id: TenantId,
74 barrier_mode: BarrierMode,
75 ) -> Vec<TenantRef> {
76 let mut ancestors = Vec::new();
77 let mut visited = HashSet::new();
78 visited.insert(id);
79
80 let Some(tenant) = self.tenants.get(&id) else {
82 return ancestors;
83 };
84
85 if barrier_mode == BarrierMode::Respect && tenant.self_managed {
87 return ancestors;
88 }
89
90 let mut current_parent_id = tenant.parent_id;
91
92 while let Some(parent_id) = current_parent_id {
93 if !visited.insert(parent_id) {
94 break; }
96
97 let Some(parent) = self.tenants.get(&parent_id) else {
98 break;
99 };
100
101 ancestors.push(parent.into());
104
105 if barrier_mode == BarrierMode::Respect && parent.self_managed {
106 break;
107 }
108
109 current_parent_id = parent.parent_id;
110 }
111
112 ancestors
113 }
114
115 pub(super) fn collect_descendants(
125 &self,
126 id: TenantId,
127 statuses: &[TenantStatus],
128 barrier_mode: BarrierMode,
129 max_depth: Option<u32>,
130 ) -> Vec<TenantRef> {
131 let mut collector = DescendantCollector {
132 tenants: &self.tenants,
133 children: &self.children,
134 statuses,
135 barrier_mode,
136 max_depth,
137 result: Vec::new(),
138 visited: HashSet::new(),
139 };
140 collector.visited.insert(id);
141 collector.collect(id, 1);
142 collector.result
143 }
144
145 pub(super) fn is_ancestor_of(
156 &self,
157 ancestor_id: TenantId,
158 descendant_id: TenantId,
159 barrier_mode: BarrierMode,
160 ) -> Result<bool, TenantResolverError> {
161 if ancestor_id == descendant_id {
163 if self.tenants.contains_key(&ancestor_id) {
164 return Ok(false);
165 }
166 return Err(TenantResolverError::TenantNotFound {
167 tenant_id: ancestor_id,
168 });
169 }
170
171 if !self.tenants.contains_key(&ancestor_id) {
173 return Err(TenantResolverError::TenantNotFound {
174 tenant_id: ancestor_id,
175 });
176 }
177
178 let descendant =
179 self.tenants
180 .get(&descendant_id)
181 .ok_or(TenantResolverError::TenantNotFound {
182 tenant_id: descendant_id,
183 })?;
184
185 if barrier_mode == BarrierMode::Respect && descendant.self_managed {
188 return Ok(false);
189 }
190
191 let mut visited = HashSet::new();
193 visited.insert(descendant_id);
194 let mut current_parent_id = descendant.parent_id;
195
196 while let Some(parent_id) = current_parent_id {
197 if !visited.insert(parent_id) {
198 break; }
200
201 let Some(parent) = self.tenants.get(&parent_id) else {
202 break;
203 };
204
205 if parent_id == ancestor_id {
207 return Ok(true);
208 }
209
210 if barrier_mode == BarrierMode::Respect && parent.self_managed {
213 return Ok(false);
214 }
215
216 current_parent_id = parent.parent_id;
217 }
218
219 Ok(false)
221 }
222}
223
224#[domain_model]
228struct DescendantCollector<'a> {
229 tenants: &'a HashMap<TenantId, TenantInfo>,
230 children: &'a HashMap<TenantId, Vec<TenantId>>,
231 statuses: &'a [TenantStatus],
232 barrier_mode: BarrierMode,
233 max_depth: Option<u32>,
234 result: Vec<TenantRef>,
235 visited: HashSet<TenantId>,
236}
237
238impl DescendantCollector<'_> {
239 fn collect(&mut self, parent_id: TenantId, current_depth: u32) {
240 if self.max_depth.is_some_and(|d| current_depth > d) {
242 return;
243 }
244
245 let Some(child_ids) = self.children.get(&parent_id) else {
246 return;
247 };
248
249 for child_id in child_ids {
250 if !self.visited.insert(*child_id) {
251 continue;
252 }
253
254 let Some(child) = self.tenants.get(child_id) else {
255 continue;
256 };
257
258 if self.barrier_mode == BarrierMode::Respect && child.self_managed {
260 continue;
261 }
262
263 if !Service::matches_status_filter(child, self.statuses) {
265 continue;
266 }
267
268 self.result.push(child.into());
269
270 self.collect(*child_id, current_depth + 1);
272 }
273 }
274}
275
276#[cfg(test)]
277#[cfg_attr(coverage_nightly, coverage(off))]
278mod tests {
279 use super::*;
280 use crate::config::TenantConfig;
281 use tenant_resolver_sdk::TenantStatus;
282 use uuid::Uuid;
283
284 fn tenant(id: &str, name: &str, status: TenantStatus) -> TenantConfig {
286 TenantConfig {
287 id: Uuid::parse_str(id).unwrap(),
288 name: name.to_owned(),
289 status,
290 tenant_type: None,
291 parent_id: None,
292 self_managed: false,
293 }
294 }
295
296 fn tenant_with_parent(id: &str, name: &str, parent: &str) -> TenantConfig {
297 TenantConfig {
298 id: Uuid::parse_str(id).unwrap(),
299 name: name.to_owned(),
300 status: TenantStatus::Active,
301 tenant_type: None,
302 parent_id: Some(Uuid::parse_str(parent).unwrap()),
303 self_managed: false,
304 }
305 }
306
307 fn tenant_barrier(id: &str, name: &str, parent: &str) -> TenantConfig {
308 TenantConfig {
309 id: Uuid::parse_str(id).unwrap(),
310 name: name.to_owned(),
311 status: TenantStatus::Active,
312 tenant_type: None,
313 parent_id: Some(Uuid::parse_str(parent).unwrap()),
314 self_managed: true,
315 }
316 }
317
318 const TENANT_A: &str = "11111111-1111-1111-1111-111111111111";
320 const TENANT_B: &str = "22222222-2222-2222-2222-222222222222";
321 const TENANT_C: &str = "33333333-3333-3333-3333-333333333333";
322 const TENANT_D: &str = "44444444-4444-4444-4444-444444444444";
323
324 #[test]
327 fn from_config_empty() {
328 let cfg = StaticTrPluginConfig::default();
329 let service = Service::from_config(&cfg);
330
331 assert!(service.tenants.is_empty());
332 assert!(service.children.is_empty());
333 }
334
335 #[test]
336 fn from_config_with_tenants_only() {
337 let cfg = StaticTrPluginConfig {
338 tenants: vec![
339 tenant(TENANT_A, "Tenant A", TenantStatus::Active),
340 tenant(TENANT_B, "Tenant B", TenantStatus::Suspended),
341 ],
342 ..Default::default()
343 };
344 let service = Service::from_config(&cfg);
345
346 assert_eq!(service.tenants.len(), 2);
347 assert!(service.children.is_empty()); let a = service
350 .tenants
351 .get(&Uuid::parse_str(TENANT_A).unwrap())
352 .unwrap();
353 assert_eq!(a.name, "Tenant A");
354 assert_eq!(a.status, TenantStatus::Active);
355 assert!(a.parent_id.is_none());
356 assert!(!a.self_managed);
357 }
358
359 #[test]
360 fn from_config_with_hierarchy() {
361 let cfg = StaticTrPluginConfig {
363 tenants: vec![
364 tenant(TENANT_A, "Root", TenantStatus::Active),
365 tenant_with_parent(TENANT_B, "Child", TENANT_A),
366 tenant_with_parent(TENANT_C, "Grandchild", TENANT_B),
367 ],
368 ..Default::default()
369 };
370 let service = Service::from_config(&cfg);
371
372 assert_eq!(service.tenants.len(), 3);
373
374 let a_id = Uuid::parse_str(TENANT_A).unwrap();
376 let b_id = Uuid::parse_str(TENANT_B).unwrap();
377 let c_id = Uuid::parse_str(TENANT_C).unwrap();
378
379 let a_children = service.children.get(&a_id).unwrap();
380 assert_eq!(a_children.len(), 1);
381 assert!(a_children.contains(&b_id));
382
383 let b_children = service.children.get(&b_id).unwrap();
384 assert_eq!(b_children.len(), 1);
385 assert!(b_children.contains(&c_id));
386
387 assert!(!service.children.contains_key(&c_id));
389 }
390
391 #[test]
394 fn collect_ancestors_root_tenant() {
395 let cfg = StaticTrPluginConfig {
396 tenants: vec![tenant(TENANT_A, "Root", TenantStatus::Active)],
397 ..Default::default()
398 };
399 let service = Service::from_config(&cfg);
400 let a_id = Uuid::parse_str(TENANT_A).unwrap();
401
402 let ancestors = service.collect_ancestors(a_id, BarrierMode::Respect);
403 assert!(ancestors.is_empty());
404 }
405
406 #[test]
407 fn collect_ancestors_linear_hierarchy() {
408 let cfg = StaticTrPluginConfig {
410 tenants: vec![
411 tenant(TENANT_A, "Root", TenantStatus::Active),
412 tenant_with_parent(TENANT_B, "Child", TENANT_A),
413 tenant_with_parent(TENANT_C, "Grandchild", TENANT_B),
414 ],
415 ..Default::default()
416 };
417 let service = Service::from_config(&cfg);
418
419 let a_id = Uuid::parse_str(TENANT_A).unwrap();
420 let b_id = Uuid::parse_str(TENANT_B).unwrap();
421 let c_id = Uuid::parse_str(TENANT_C).unwrap();
422
423 let ancestors = service.collect_ancestors(c_id, BarrierMode::Respect);
425 assert_eq!(ancestors.len(), 2);
426 assert_eq!(ancestors[0].id, b_id);
427 assert_eq!(ancestors[1].id, a_id);
428
429 let ancestors = service.collect_ancestors(b_id, BarrierMode::Respect);
431 assert_eq!(ancestors.len(), 1);
432 assert_eq!(ancestors[0].id, a_id);
433 }
434
435 #[test]
436 fn collect_ancestors_with_barrier() {
437 let cfg = StaticTrPluginConfig {
439 tenants: vec![
440 tenant(TENANT_A, "Root", TenantStatus::Active),
441 tenant_barrier(TENANT_B, "Barrier", TENANT_A),
442 tenant_with_parent(TENANT_C, "Grandchild", TENANT_B),
443 ],
444 ..Default::default()
445 };
446 let service = Service::from_config(&cfg);
447
448 let a_id = Uuid::parse_str(TENANT_A).unwrap();
449 let b_id = Uuid::parse_str(TENANT_B).unwrap();
450 let c_id = Uuid::parse_str(TENANT_C).unwrap();
451
452 let ancestors = service.collect_ancestors(c_id, BarrierMode::Respect);
454 assert_eq!(ancestors.len(), 1);
455 assert_eq!(ancestors[0].id, b_id);
456
457 let ancestors = service.collect_ancestors(c_id, BarrierMode::Ignore);
459 assert_eq!(ancestors.len(), 2);
460 assert_eq!(ancestors[0].id, b_id);
461 assert_eq!(ancestors[1].id, a_id);
462 }
463
464 #[test]
465 fn collect_ancestors_starting_tenant_is_barrier() {
466 let cfg = StaticTrPluginConfig {
469 tenants: vec![
470 tenant(TENANT_A, "Root", TenantStatus::Active),
471 tenant_barrier(TENANT_B, "Barrier", TENANT_A),
472 ],
473 ..Default::default()
474 };
475 let service = Service::from_config(&cfg);
476
477 let a_id = Uuid::parse_str(TENANT_A).unwrap();
478 let b_id = Uuid::parse_str(TENANT_B).unwrap();
479
480 let ancestors = service.collect_ancestors(b_id, BarrierMode::Respect);
482 assert!(ancestors.is_empty());
483
484 let ancestors = service.collect_ancestors(b_id, BarrierMode::Ignore);
486 assert_eq!(ancestors.len(), 1);
487 assert_eq!(ancestors[0].id, a_id);
488 }
489
490 #[test]
493 fn collect_descendants_no_children() {
494 let cfg = StaticTrPluginConfig {
495 tenants: vec![tenant(TENANT_A, "Root", TenantStatus::Active)],
496 ..Default::default()
497 };
498 let service = Service::from_config(&cfg);
499 let a_id = Uuid::parse_str(TENANT_A).unwrap();
500
501 let descendants = service.collect_descendants(a_id, &[], BarrierMode::Respect, None);
502 assert!(descendants.is_empty());
503 }
504
505 #[test]
506 fn collect_descendants_linear_hierarchy() {
507 let cfg = StaticTrPluginConfig {
509 tenants: vec![
510 tenant(TENANT_A, "Root", TenantStatus::Active),
511 tenant_with_parent(TENANT_B, "Child", TENANT_A),
512 tenant_with_parent(TENANT_C, "Grandchild", TENANT_B),
513 ],
514 ..Default::default()
515 };
516 let service = Service::from_config(&cfg);
517
518 let a_id = Uuid::parse_str(TENANT_A).unwrap();
519 let b_id = Uuid::parse_str(TENANT_B).unwrap();
520 let c_id = Uuid::parse_str(TENANT_C).unwrap();
521
522 let descendants = service.collect_descendants(a_id, &[], BarrierMode::Respect, None);
524 assert_eq!(descendants.len(), 2);
525 assert_eq!(descendants[0].id, b_id);
527 assert_eq!(descendants[1].id, c_id);
528
529 let descendants = service.collect_descendants(a_id, &[], BarrierMode::Respect, Some(1));
531 assert_eq!(descendants.len(), 1);
532 assert_eq!(descendants[0].id, b_id);
533 }
534
535 #[test]
536 fn collect_descendants_with_barrier() {
537 let cfg = StaticTrPluginConfig {
539 tenants: vec![
540 tenant(TENANT_A, "Root", TenantStatus::Active),
541 tenant_barrier(TENANT_B, "Barrier", TENANT_A),
542 tenant_with_parent(TENANT_C, "Grandchild", TENANT_B),
543 ],
544 ..Default::default()
545 };
546 let service = Service::from_config(&cfg);
547
548 let a_id = Uuid::parse_str(TENANT_A).unwrap();
549 let b_id = Uuid::parse_str(TENANT_B).unwrap();
550 let c_id = Uuid::parse_str(TENANT_C).unwrap();
551
552 let descendants = service.collect_descendants(a_id, &[], BarrierMode::Respect, None);
554 assert!(descendants.is_empty());
555
556 let descendants = service.collect_descendants(a_id, &[], BarrierMode::Ignore, None);
558 assert_eq!(descendants.len(), 2);
559 assert_eq!(descendants[0].id, b_id);
560 assert_eq!(descendants[1].id, c_id);
561 }
562
563 #[test]
564 fn collect_descendants_mixed_barrier() {
565 let cfg = StaticTrPluginConfig {
568 tenants: vec![
569 tenant(TENANT_A, "Root", TenantStatus::Active),
570 tenant_barrier(TENANT_B, "Barrier", TENANT_A),
571 tenant_with_parent(TENANT_C, "Under Barrier", TENANT_B),
572 tenant_with_parent(TENANT_D, "Normal Child", TENANT_A),
573 ],
574 ..Default::default()
575 };
576 let service = Service::from_config(&cfg);
577
578 let a_id = Uuid::parse_str(TENANT_A).unwrap();
579 let d_id = Uuid::parse_str(TENANT_D).unwrap();
580
581 let descendants = service.collect_descendants(a_id, &[], BarrierMode::Respect, None);
583 assert_eq!(descendants.len(), 1);
584 assert_eq!(descendants[0].id, d_id);
585 }
586
587 #[test]
590 fn is_ancestor_of_self() {
591 let cfg = StaticTrPluginConfig {
592 tenants: vec![tenant(TENANT_A, "Root", TenantStatus::Active)],
593 ..Default::default()
594 };
595 let service = Service::from_config(&cfg);
596 let a_id = Uuid::parse_str(TENANT_A).unwrap();
597
598 assert!(
600 !service
601 .is_ancestor_of(a_id, a_id, BarrierMode::Respect)
602 .unwrap()
603 );
604 }
605
606 #[test]
607 fn is_ancestor_of_direct_parent() {
608 let cfg = StaticTrPluginConfig {
609 tenants: vec![
610 tenant(TENANT_A, "Root", TenantStatus::Active),
611 tenant_with_parent(TENANT_B, "Child", TENANT_A),
612 ],
613 ..Default::default()
614 };
615 let service = Service::from_config(&cfg);
616
617 let a_id = Uuid::parse_str(TENANT_A).unwrap();
618 let b_id = Uuid::parse_str(TENANT_B).unwrap();
619
620 assert!(
621 service
622 .is_ancestor_of(a_id, b_id, BarrierMode::Respect)
623 .unwrap()
624 );
625 assert!(
626 !service
627 .is_ancestor_of(b_id, a_id, BarrierMode::Respect)
628 .unwrap()
629 );
630 }
631
632 #[test]
633 fn is_ancestor_of_grandparent() {
634 let cfg = StaticTrPluginConfig {
636 tenants: vec![
637 tenant(TENANT_A, "Root", TenantStatus::Active),
638 tenant_with_parent(TENANT_B, "Child", TENANT_A),
639 tenant_with_parent(TENANT_C, "Grandchild", TENANT_B),
640 ],
641 ..Default::default()
642 };
643 let service = Service::from_config(&cfg);
644
645 let a_id = Uuid::parse_str(TENANT_A).unwrap();
646 let c_id = Uuid::parse_str(TENANT_C).unwrap();
647
648 assert!(
649 service
650 .is_ancestor_of(a_id, c_id, BarrierMode::Respect)
651 .unwrap()
652 );
653 }
654
655 #[test]
656 fn is_ancestor_of_with_barrier() {
657 let cfg = StaticTrPluginConfig {
659 tenants: vec![
660 tenant(TENANT_A, "Root", TenantStatus::Active),
661 tenant_barrier(TENANT_B, "Barrier", TENANT_A),
662 tenant_with_parent(TENANT_C, "Grandchild", TENANT_B),
663 ],
664 ..Default::default()
665 };
666 let service = Service::from_config(&cfg);
667
668 let a_id = Uuid::parse_str(TENANT_A).unwrap();
669 let b_id = Uuid::parse_str(TENANT_B).unwrap();
670 let c_id = Uuid::parse_str(TENANT_C).unwrap();
671
672 assert!(
674 service
675 .is_ancestor_of(b_id, c_id, BarrierMode::Respect)
676 .unwrap()
677 );
678
679 assert!(
681 !service
682 .is_ancestor_of(a_id, c_id, BarrierMode::Respect)
683 .unwrap()
684 );
685
686 assert!(
688 service
689 .is_ancestor_of(a_id, c_id, BarrierMode::Ignore)
690 .unwrap()
691 );
692 }
693
694 #[test]
695 fn is_ancestor_of_direct_barrier_child() {
696 let cfg = StaticTrPluginConfig {
700 tenants: vec![
701 tenant(TENANT_A, "Root", TenantStatus::Active),
702 tenant_barrier(TENANT_B, "Barrier", TENANT_A),
703 ],
704 ..Default::default()
705 };
706 let service = Service::from_config(&cfg);
707
708 let a_id = Uuid::parse_str(TENANT_A).unwrap();
709 let b_id = Uuid::parse_str(TENANT_B).unwrap();
710
711 assert!(
713 !service
714 .is_ancestor_of(a_id, b_id, BarrierMode::Respect)
715 .unwrap()
716 );
717
718 assert!(
720 service
721 .is_ancestor_of(a_id, b_id, BarrierMode::Ignore)
722 .unwrap()
723 );
724
725 assert!(
727 !service
728 .is_ancestor_of(b_id, b_id, BarrierMode::Respect)
729 .unwrap()
730 );
731 }
732
733 #[test]
734 fn is_ancestor_of_nonexistent() {
735 let cfg = StaticTrPluginConfig {
736 tenants: vec![tenant(TENANT_A, "Root", TenantStatus::Active)],
737 ..Default::default()
738 };
739 let service = Service::from_config(&cfg);
740
741 let a_id = Uuid::parse_str(TENANT_A).unwrap();
742 let nonexistent = Uuid::parse_str(TENANT_B).unwrap();
743
744 assert!(matches!(
746 service.is_ancestor_of(nonexistent, a_id, BarrierMode::Respect),
747 Err(TenantResolverError::TenantNotFound { tenant_id }) if tenant_id == nonexistent
748 ));
749
750 assert!(matches!(
752 service.is_ancestor_of(a_id, nonexistent, BarrierMode::Respect),
753 Err(TenantResolverError::TenantNotFound { tenant_id }) if tenant_id == nonexistent
754 ));
755 }
756
757 #[test]
758 fn collect_ancestors_cycle_terminates() {
759 let a_id = Uuid::parse_str(TENANT_A).unwrap();
761 let b_id = Uuid::parse_str(TENANT_B).unwrap();
762
763 let cfg = StaticTrPluginConfig {
764 tenants: vec![
765 TenantConfig {
766 id: a_id,
767 name: "A".to_owned(),
768 status: TenantStatus::Active,
769 tenant_type: None,
770 parent_id: Some(b_id),
771 self_managed: false,
772 },
773 TenantConfig {
774 id: b_id,
775 name: "B".to_owned(),
776 status: TenantStatus::Active,
777 tenant_type: None,
778 parent_id: Some(a_id),
779 self_managed: false,
780 },
781 ],
782 ..Default::default()
783 };
784 let service = Service::from_config(&cfg);
785
786 let ancestors = service.collect_ancestors(a_id, BarrierMode::Ignore);
788 assert!(ancestors.len() <= 2);
789 }
790
791 #[test]
792 fn is_ancestor_of_cycle_terminates() {
793 let a_id = Uuid::parse_str(TENANT_A).unwrap();
795 let b_id = Uuid::parse_str(TENANT_B).unwrap();
796 let c_id = Uuid::parse_str(TENANT_C).unwrap();
797
798 let cfg = StaticTrPluginConfig {
799 tenants: vec![
800 TenantConfig {
801 id: a_id,
802 name: "A".to_owned(),
803 status: TenantStatus::Active,
804 tenant_type: None,
805 parent_id: Some(b_id),
806 self_managed: false,
807 },
808 TenantConfig {
809 id: b_id,
810 name: "B".to_owned(),
811 status: TenantStatus::Active,
812 tenant_type: None,
813 parent_id: Some(a_id),
814 self_managed: false,
815 },
816 TenantConfig {
817 id: c_id,
818 name: "C".to_owned(),
819 status: TenantStatus::Active,
820 tenant_type: None,
821 parent_id: None,
822 self_managed: false,
823 },
824 ],
825 ..Default::default()
826 };
827 let service = Service::from_config(&cfg);
828
829 assert!(
831 !service
832 .is_ancestor_of(c_id, a_id, BarrierMode::Ignore)
833 .unwrap()
834 );
835 }
836
837 #[test]
838 fn is_ancestor_of_unrelated() {
839 let cfg = StaticTrPluginConfig {
841 tenants: vec![
842 tenant(TENANT_A, "Root A", TenantStatus::Active),
843 tenant(TENANT_B, "Root B", TenantStatus::Active),
844 ],
845 ..Default::default()
846 };
847 let service = Service::from_config(&cfg);
848
849 let a_id = Uuid::parse_str(TENANT_A).unwrap();
850 let b_id = Uuid::parse_str(TENANT_B).unwrap();
851
852 assert!(
853 !service
854 .is_ancestor_of(a_id, b_id, BarrierMode::Respect)
855 .unwrap()
856 );
857 assert!(
858 !service
859 .is_ancestor_of(b_id, a_id, BarrierMode::Respect)
860 .unwrap()
861 );
862 }
863}