1use std::collections::{HashSet, VecDeque};
5
6use graph_engine::{Direction, GraphEngine, PropertyValue};
7use serde::{Deserialize, Serialize};
8
9use crate::{Result, VaultError};
10
11const DEPENDS_ON_EDGE: &str = "SECRET_DEPENDS_ON";
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ImpactReport {
17 pub root_secret: String,
19 pub affected_secrets: Vec<String>,
21 pub affected_agents: Vec<String>,
23 pub depth: usize,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct DependencyInfo {
30 pub parent: String,
32 pub child: String,
34 pub created_at_ms: i64,
36}
37
38#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
40pub enum DependencyWeight {
41 Critical,
43 High,
45 Medium,
47 Low,
49}
50
51impl DependencyWeight {
52 #[must_use]
54 pub fn value(self) -> f64 {
55 match self {
56 Self::Critical => 1.0,
57 Self::High => 0.7,
58 Self::Medium => 0.4,
59 Self::Low => 0.1,
60 }
61 }
62
63 fn from_float(v: f64) -> Self {
65 if v >= 0.9 {
66 Self::Critical
67 } else if v >= 0.6 {
68 Self::High
69 } else if v >= 0.3 {
70 Self::Medium
71 } else {
72 Self::Low
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct WeightedAffectedSecret {
80 pub secret: String,
82 pub depth: usize,
84 pub edge_weight: DependencyWeight,
86 pub impact_score: f64,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct WeightedImpactReport {
93 pub root_secret: String,
95 pub affected_secrets: Vec<WeightedAffectedSecret>,
97 pub affected_agents: Vec<String>,
99 pub max_depth: usize,
101 pub total_impact_score: f64,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct RotationStep {
108 pub secret: String,
110 pub depth: usize,
112 pub priority: f64,
114 pub weight: DependencyWeight,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct RotationPlan {
121 pub root_secret: String,
123 pub rotation_order: Vec<RotationStep>,
125 pub total_secrets: usize,
127}
128
129fn would_create_cycle(graph: &GraphEngine, parent_node: u64, child_node: u64) -> bool {
131 if parent_node == child_node {
134 return true;
135 }
136
137 let mut visited = HashSet::new();
139 let mut queue = VecDeque::new();
140 queue.push_back(child_node);
141 visited.insert(child_node);
142
143 while let Some(current) = queue.pop_front() {
144 if let Ok(edges) = graph.edges_of(current, Direction::Outgoing) {
145 for edge in edges {
146 if edge.edge_type != DEPENDS_ON_EDGE {
147 continue;
148 }
149 let target = if edge.from == current {
150 edge.to
151 } else {
152 edge.from
153 };
154 if target == parent_node {
155 return true;
156 }
157 if visited.insert(target) {
158 queue.push_back(target);
159 }
160 }
161 }
162 }
163
164 false
165}
166
167pub fn add_dependency(
174 graph: &GraphEngine,
175 parent_node_key: &str,
176 child_node_key: &str,
177 timestamp: i64,
178) -> Result<()> {
179 let parent_id = find_or_create_dep_node(graph, parent_node_key);
180 let child_id = find_or_create_dep_node(graph, child_node_key);
181
182 if would_create_cycle(graph, parent_id, child_id) {
183 return Err(VaultError::CyclicDependency(format!(
184 "adding dependency from '{child_node_key}' on '{parent_node_key}' would create a cycle"
185 )));
186 }
187
188 let mut props = std::collections::HashMap::new();
189 props.insert("created_at_ms".to_string(), PropertyValue::Int(timestamp));
190
191 graph
192 .create_edge(parent_id, child_id, DEPENDS_ON_EDGE, props, true)
193 .map_err(|e| VaultError::GraphError(e.to_string()))?;
194
195 Ok(())
196}
197
198pub fn remove_dependency(
204 graph: &GraphEngine,
205 parent_node_key: &str,
206 child_node_key: &str,
207) -> Result<()> {
208 let Some(parent_id) = find_dep_node(graph, parent_node_key) else {
209 return Ok(());
210 };
211 let Some(child_id) = find_dep_node(graph, child_node_key) else {
212 return Ok(());
213 };
214
215 if let Ok(edges) = graph.edges_of(parent_id, Direction::Outgoing) {
216 for edge in edges {
217 if edge.edge_type == DEPENDS_ON_EDGE && edge.to == child_id {
218 graph
219 .delete_edge(edge.id)
220 .map_err(|e| VaultError::GraphError(e.to_string()))?;
221 }
222 }
223 }
224
225 Ok(())
226}
227
228pub fn get_dependencies(graph: &GraphEngine, node_key: &str) -> Vec<String> {
230 let Some(node_id) = find_dep_node(graph, node_key) else {
231 return Vec::new();
232 };
233
234 let mut children = Vec::new();
235 if let Ok(edges) = graph.edges_of(node_id, Direction::Outgoing) {
236 for edge in edges {
237 if edge.edge_type != DEPENDS_ON_EDGE {
238 continue;
239 }
240 let target = if edge.from == node_id {
241 edge.to
242 } else {
243 edge.from
244 };
245 if let Some(key) = node_entity_key(graph, target) {
246 children.push(key);
247 }
248 }
249 }
250
251 children
252}
253
254pub fn get_dependents(graph: &GraphEngine, node_key: &str) -> Vec<String> {
256 let Some(node_id) = find_dep_node(graph, node_key) else {
257 return Vec::new();
258 };
259
260 let mut parents = Vec::new();
261 if let Ok(edges) = graph.edges_of(node_id, Direction::Incoming) {
262 for edge in edges {
263 if edge.edge_type != DEPENDS_ON_EDGE {
264 continue;
265 }
266 let source = if edge.to == node_id {
267 edge.from
268 } else {
269 edge.to
270 };
271 if let Some(key) = node_entity_key(graph, source) {
272 parents.push(key);
273 }
274 }
275 }
276
277 parents
278}
279
280pub fn impact_analysis(graph: &GraphEngine, node_key: &str) -> ImpactReport {
282 let mut affected = Vec::new();
283 let mut max_depth = 0;
284
285 let Some(start_id) = find_dep_node(graph, node_key) else {
286 return ImpactReport {
287 root_secret: node_key.to_string(),
288 affected_secrets: Vec::new(),
289 affected_agents: Vec::new(),
290 depth: 0,
291 };
292 };
293
294 let mut visited = HashSet::new();
295 visited.insert(start_id);
296 let mut queue: VecDeque<(u64, usize)> = VecDeque::new();
297 queue.push_back((start_id, 0));
298
299 while let Some((current, depth)) = queue.pop_front() {
300 if let Ok(edges) = graph.edges_of(current, Direction::Outgoing) {
301 for edge in edges {
302 if edge.edge_type != DEPENDS_ON_EDGE {
303 continue;
304 }
305 let target = if edge.from == current {
306 edge.to
307 } else {
308 edge.from
309 };
310 if visited.insert(target) {
311 let new_depth = depth + 1;
312 if new_depth > max_depth {
313 max_depth = new_depth;
314 }
315 if let Some(key) = node_entity_key(graph, target) {
316 affected.push(key);
317 }
318 queue.push_back((target, new_depth));
319 }
320 }
321 }
322 }
323
324 let mut agents = HashSet::new();
326 for node_id in &visited {
327 if let Ok(edges) = graph.edges_of(*node_id, Direction::Incoming) {
328 for edge in edges {
329 if edge.edge_type.starts_with("VAULT_ACCESS") {
330 let source = if edge.to == *node_id {
331 edge.from
332 } else {
333 edge.to
334 };
335 if let Some(key) = node_entity_key(graph, source) {
336 if !key.starts_with("vault_secret:") {
337 agents.insert(key);
338 }
339 }
340 }
341 }
342 }
343 }
344
345 ImpactReport {
346 root_secret: node_key.to_string(),
347 affected_secrets: affected,
348 affected_agents: agents.into_iter().collect(),
349 depth: max_depth,
350 }
351}
352
353pub fn add_weighted_dependency(
360 graph: &GraphEngine,
361 parent_key: &str,
362 child_key: &str,
363 weight: DependencyWeight,
364 description: Option<&str>,
365 timestamp: i64,
366) -> Result<()> {
367 let parent_id = find_or_create_dep_node(graph, parent_key);
368 let child_id = find_or_create_dep_node(graph, child_key);
369
370 if would_create_cycle(graph, parent_id, child_id) {
371 return Err(VaultError::CyclicDependency(format!(
372 "adding dependency from '{child_key}' on '{parent_key}' would create a cycle"
373 )));
374 }
375
376 let mut props = std::collections::HashMap::new();
377 props.insert("created_at_ms".to_string(), PropertyValue::Int(timestamp));
378 props.insert(
379 "dep_weight".to_string(),
380 PropertyValue::Float(weight.value()),
381 );
382 if let Some(desc) = description {
383 props.insert(
384 "dep_desc".to_string(),
385 PropertyValue::String(desc.to_string()),
386 );
387 }
388
389 graph
390 .create_edge(parent_id, child_id, DEPENDS_ON_EDGE, props, true)
391 .map_err(|e| VaultError::GraphError(e.to_string()))?;
392
393 Ok(())
394}
395
396#[must_use]
398pub fn weighted_impact_analysis(graph: &GraphEngine, node_key: &str) -> WeightedImpactReport {
399 let Some(start_id) = find_dep_node(graph, node_key) else {
400 return WeightedImpactReport {
401 root_secret: node_key.to_string(),
402 affected_secrets: Vec::new(),
403 affected_agents: Vec::new(),
404 max_depth: 0,
405 total_impact_score: 0.0,
406 };
407 };
408
409 let mut affected = Vec::new();
410 let mut max_depth = 0;
411 let mut total_impact = 0.0;
412
413 let mut visited = HashSet::new();
414 visited.insert(start_id);
415 let mut queue: VecDeque<(u64, usize)> = VecDeque::new();
416 queue.push_back((start_id, 0));
417
418 while let Some((current, depth)) = queue.pop_front() {
419 if let Ok(edges) = graph.edges_of(current, Direction::Outgoing) {
420 for edge in edges {
421 if edge.edge_type != DEPENDS_ON_EDGE {
422 continue;
423 }
424 let target = if edge.from == current {
425 edge.to
426 } else {
427 edge.from
428 };
429 if visited.insert(target) {
430 let new_depth = depth + 1;
431 if new_depth > max_depth {
432 max_depth = new_depth;
433 }
434
435 let weight = edge
436 .properties
437 .get("dep_weight")
438 .and_then(|p| match p {
439 PropertyValue::Float(f) => Some(DependencyWeight::from_float(*f)),
440 _ => None,
441 })
442 .unwrap_or(DependencyWeight::Medium);
443
444 #[allow(clippy::cast_precision_loss)] let impact_score = weight.value() / new_depth as f64;
446 total_impact += impact_score;
447
448 if let Some(key) = node_entity_key(graph, target) {
449 affected.push(WeightedAffectedSecret {
450 secret: key,
451 depth: new_depth,
452 edge_weight: weight,
453 impact_score,
454 });
455 }
456 queue.push_back((target, new_depth));
457 }
458 }
459 }
460 }
461
462 let mut agents = HashSet::new();
464 for node_id in &visited {
465 if let Ok(edges) = graph.edges_of(*node_id, Direction::Incoming) {
466 for edge in edges {
467 if edge.edge_type.starts_with("VAULT_ACCESS") {
468 let source = if edge.to == *node_id {
469 edge.from
470 } else {
471 edge.to
472 };
473 if let Some(key) = node_entity_key(graph, source) {
474 if !key.starts_with("vault_secret:") {
475 agents.insert(key);
476 }
477 }
478 }
479 }
480 }
481 }
482
483 WeightedImpactReport {
484 root_secret: node_key.to_string(),
485 affected_secrets: affected,
486 affected_agents: agents.into_iter().collect(),
487 max_depth,
488 total_impact_score: total_impact,
489 }
490}
491
492#[must_use]
494pub fn rotation_plan(graph: &GraphEngine, root_key: &str) -> RotationPlan {
495 let report = weighted_impact_analysis(graph, root_key);
496
497 let mut steps: Vec<RotationStep> = report
498 .affected_secrets
499 .into_iter()
500 .map(|s| RotationStep {
501 secret: s.secret,
502 depth: s.depth,
503 priority: s.impact_score,
504 weight: s.edge_weight,
505 })
506 .collect();
507
508 steps.sort_by(|a, b| {
510 b.priority
511 .partial_cmp(&a.priority)
512 .unwrap_or(std::cmp::Ordering::Equal)
513 });
514
515 let total = steps.len();
516 RotationPlan {
517 root_secret: root_key.to_string(),
518 rotation_order: steps,
519 total_secrets: total,
520 }
521}
522
523fn find_dep_node(graph: &GraphEngine, key: &str) -> Option<u64> {
524 graph
525 .find_nodes_by_property("entity_key", &PropertyValue::String(key.to_string()))
526 .ok()
527 .and_then(|nodes| nodes.first().map(|n| n.id))
528}
529
530fn find_or_create_dep_node(graph: &GraphEngine, key: &str) -> u64 {
531 if let Some(id) = find_dep_node(graph, key) {
532 return id;
533 }
534
535 let mut props = std::collections::HashMap::new();
536 props.insert(
537 "entity_key".to_string(),
538 PropertyValue::String(key.to_string()),
539 );
540 graph.create_node("VaultEntity", props).unwrap_or(0)
541}
542
543fn node_entity_key(graph: &GraphEngine, node_id: u64) -> Option<String> {
544 graph.get_node(node_id).ok().and_then(|node| {
545 if let Some(PropertyValue::String(key)) = node.properties.get("entity_key") {
546 Some(key.clone())
547 } else {
548 None
549 }
550 })
551}
552
553#[cfg(test)]
554mod tests {
555 use std::sync::Arc;
556
557 use super::*;
558
559 fn test_graph() -> Arc<GraphEngine> {
560 Arc::new(GraphEngine::new())
561 }
562
563 #[test]
564 fn test_add_and_get_dependencies() {
565 let graph = test_graph();
566 add_dependency(&graph, "secret:db_password", "secret:app_config", 1000).unwrap();
567
568 let children = get_dependencies(&graph, "secret:db_password");
569 assert_eq!(children, vec!["secret:app_config"]);
570
571 let parents = get_dependents(&graph, "secret:app_config");
572 assert_eq!(parents, vec!["secret:db_password"]);
573 }
574
575 #[test]
576 fn test_remove_dependency() {
577 let graph = test_graph();
578 add_dependency(&graph, "secret:a", "secret:b", 1000).unwrap();
579 assert_eq!(get_dependencies(&graph, "secret:a").len(), 1);
580
581 remove_dependency(&graph, "secret:a", "secret:b").unwrap();
582 assert!(get_dependencies(&graph, "secret:a").is_empty());
583 }
584
585 #[test]
586 fn test_cycle_detection_self_reference() {
587 let graph = test_graph();
588 let result = add_dependency(&graph, "secret:a", "secret:a", 1000);
589 assert!(matches!(result, Err(VaultError::CyclicDependency(_))));
590 }
591
592 #[test]
593 fn test_cycle_detection_two_node() {
594 let graph = test_graph();
595 add_dependency(&graph, "secret:a", "secret:b", 1000).unwrap();
596 let result = add_dependency(&graph, "secret:b", "secret:a", 2000);
597 assert!(matches!(result, Err(VaultError::CyclicDependency(_))));
598 }
599
600 #[test]
601 fn test_cycle_detection_three_node() {
602 let graph = test_graph();
603 add_dependency(&graph, "secret:a", "secret:b", 1000).unwrap();
604 add_dependency(&graph, "secret:b", "secret:c", 2000).unwrap();
605 let result = add_dependency(&graph, "secret:c", "secret:a", 3000);
606 assert!(matches!(result, Err(VaultError::CyclicDependency(_))));
607 }
608
609 #[test]
610 fn test_impact_analysis_single_level() {
611 let graph = test_graph();
612 add_dependency(&graph, "secret:root", "secret:child1", 1000).unwrap();
613 add_dependency(&graph, "secret:root", "secret:child2", 2000).unwrap();
614
615 let report = impact_analysis(&graph, "secret:root");
616 assert_eq!(report.root_secret, "secret:root");
617 assert_eq!(report.affected_secrets.len(), 2);
618 assert_eq!(report.depth, 1);
619 }
620
621 #[test]
622 fn test_impact_analysis_transitive() {
623 let graph = test_graph();
624 add_dependency(&graph, "secret:a", "secret:b", 1000).unwrap();
625 add_dependency(&graph, "secret:b", "secret:c", 2000).unwrap();
626 add_dependency(&graph, "secret:c", "secret:d", 3000).unwrap();
627
628 let report = impact_analysis(&graph, "secret:a");
629 assert_eq!(report.affected_secrets.len(), 3);
630 assert_eq!(report.depth, 3);
631 }
632
633 #[test]
634 fn test_impact_analysis_diamond() {
635 let graph = test_graph();
636 add_dependency(&graph, "secret:a", "secret:b", 1000).unwrap();
638 add_dependency(&graph, "secret:a", "secret:c", 2000).unwrap();
639 add_dependency(&graph, "secret:b", "secret:d", 3000).unwrap();
640 add_dependency(&graph, "secret:c", "secret:d", 4000).unwrap();
641
642 let report = impact_analysis(&graph, "secret:a");
643 assert_eq!(report.affected_secrets.len(), 3); assert_eq!(report.depth, 2);
645 }
646
647 #[test]
648 fn test_impact_analysis_no_deps() {
649 let graph = test_graph();
650 let report = impact_analysis(&graph, "secret:isolated");
651 assert!(report.affected_secrets.is_empty());
652 assert_eq!(report.depth, 0);
653 }
654
655 #[test]
656 fn test_multiple_children() {
657 let graph = test_graph();
658 add_dependency(&graph, "secret:parent", "secret:c1", 1000).unwrap();
659 add_dependency(&graph, "secret:parent", "secret:c2", 2000).unwrap();
660 add_dependency(&graph, "secret:parent", "secret:c3", 3000).unwrap();
661
662 let children = get_dependencies(&graph, "secret:parent");
663 assert_eq!(children.len(), 3);
664 }
665
666 #[test]
667 fn test_multiple_parents() {
668 let graph = test_graph();
669 add_dependency(&graph, "secret:p1", "secret:child", 1000).unwrap();
670 add_dependency(&graph, "secret:p2", "secret:child", 2000).unwrap();
671
672 let parents = get_dependents(&graph, "secret:child");
673 assert_eq!(parents.len(), 2);
674 }
675
676 #[test]
677 fn test_get_deps_nonexistent() {
678 let graph = test_graph();
679 let children = get_dependencies(&graph, "secret:nonexistent");
680 assert!(children.is_empty());
681 }
682
683 #[test]
684 fn test_remove_nonexistent_dependency() {
685 let graph = test_graph();
686 remove_dependency(&graph, "secret:x", "secret:y").unwrap();
688 }
689
690 #[test]
691 fn test_no_false_positive_cycles_for_siblings() {
692 let graph = test_graph();
693 add_dependency(&graph, "secret:a", "secret:b", 1000).unwrap();
695 add_dependency(&graph, "secret:a", "secret:c", 2000).unwrap();
696 add_dependency(&graph, "secret:b", "secret:d", 3000).unwrap();
697 add_dependency(&graph, "secret:c", "secret:d", 4000).unwrap();
698 }
699
700 #[test]
701 fn test_dependency_info_structure() {
702 let info = DependencyInfo {
703 parent: "secret:parent".to_string(),
704 child: "secret:child".to_string(),
705 created_at_ms: 12345,
706 };
707 assert_eq!(info.parent, "secret:parent");
708 assert_eq!(info.child, "secret:child");
709 assert_eq!(info.created_at_ms, 12345);
710 }
711
712 #[test]
713 fn test_impact_report_serialization() {
714 let report = ImpactReport {
715 root_secret: "secret:root".to_string(),
716 affected_secrets: vec!["secret:a".to_string()],
717 affected_agents: vec!["user:alice".to_string()],
718 depth: 1,
719 };
720 let json = serde_json::to_string(&report).unwrap();
721 let deserialized: ImpactReport = serde_json::from_str(&json).unwrap();
722 assert_eq!(deserialized.root_secret, report.root_secret);
723 }
724
725 #[test]
726 fn test_add_weighted_dep() {
727 let graph = test_graph();
728 add_weighted_dependency(
729 &graph,
730 "secret:a",
731 "secret:b",
732 DependencyWeight::Critical,
733 Some("critical link"),
734 1000,
735 )
736 .unwrap();
737
738 let children = get_dependencies(&graph, "secret:a");
739 assert_eq!(children, vec!["secret:b"]);
740 }
741
742 #[test]
743 fn test_weighted_cycle_detection() {
744 let graph = test_graph();
745 add_weighted_dependency(
746 &graph,
747 "secret:a",
748 "secret:b",
749 DependencyWeight::High,
750 None,
751 1000,
752 )
753 .unwrap();
754 let result = add_weighted_dependency(
755 &graph,
756 "secret:b",
757 "secret:a",
758 DependencyWeight::High,
759 None,
760 2000,
761 );
762 assert!(matches!(result, Err(VaultError::CyclicDependency(_))));
763 }
764
765 #[test]
766 fn test_weighted_impact_single() {
767 let graph = test_graph();
768 add_weighted_dependency(
769 &graph,
770 "secret:root",
771 "secret:child",
772 DependencyWeight::Critical,
773 None,
774 1000,
775 )
776 .unwrap();
777
778 let report = weighted_impact_analysis(&graph, "secret:root");
779 assert_eq!(report.affected_secrets.len(), 1);
780 assert_eq!(report.affected_secrets[0].secret, "secret:child");
781 assert_eq!(report.affected_secrets[0].depth, 1);
782 assert!((report.affected_secrets[0].edge_weight.value() - 1.0).abs() < f64::EPSILON);
783 }
784
785 #[test]
786 fn test_weighted_impact_mixed() {
787 let graph = test_graph();
788 add_weighted_dependency(
789 &graph,
790 "secret:a",
791 "secret:b",
792 DependencyWeight::Critical,
793 None,
794 1000,
795 )
796 .unwrap();
797 add_weighted_dependency(
798 &graph,
799 "secret:a",
800 "secret:c",
801 DependencyWeight::Low,
802 None,
803 2000,
804 )
805 .unwrap();
806
807 let report = weighted_impact_analysis(&graph, "secret:a");
808 assert_eq!(report.affected_secrets.len(), 2);
809 assert!(report.total_impact_score > 0.0);
810 }
811
812 #[test]
813 fn test_impact_score_calculation() {
814 let graph = test_graph();
815 add_weighted_dependency(
816 &graph,
817 "secret:a",
818 "secret:b",
819 DependencyWeight::Critical,
820 None,
821 1000,
822 )
823 .unwrap();
824
825 let report = weighted_impact_analysis(&graph, "secret:a");
826 assert!((report.affected_secrets[0].impact_score - 1.0).abs() < f64::EPSILON);
828 }
829
830 #[test]
831 fn test_rotation_plan_chain() {
832 let graph = test_graph();
833 add_weighted_dependency(
834 &graph,
835 "secret:a",
836 "secret:b",
837 DependencyWeight::Critical,
838 None,
839 1000,
840 )
841 .unwrap();
842 add_weighted_dependency(
843 &graph,
844 "secret:b",
845 "secret:c",
846 DependencyWeight::High,
847 None,
848 2000,
849 )
850 .unwrap();
851
852 let plan = rotation_plan(&graph, "secret:a");
853 assert_eq!(plan.total_secrets, 2);
854 assert_eq!(plan.rotation_order.len(), 2);
855 }
856
857 #[test]
858 fn test_rotation_plan_diamond() {
859 let graph = test_graph();
860 add_weighted_dependency(
861 &graph,
862 "secret:a",
863 "secret:b",
864 DependencyWeight::High,
865 None,
866 1000,
867 )
868 .unwrap();
869 add_weighted_dependency(
870 &graph,
871 "secret:a",
872 "secret:c",
873 DependencyWeight::Medium,
874 None,
875 2000,
876 )
877 .unwrap();
878 add_weighted_dependency(
879 &graph,
880 "secret:b",
881 "secret:d",
882 DependencyWeight::Low,
883 None,
884 3000,
885 )
886 .unwrap();
887 add_weighted_dependency(
888 &graph,
889 "secret:c",
890 "secret:d",
891 DependencyWeight::Low,
892 None,
893 4000,
894 )
895 .unwrap();
896
897 let plan = rotation_plan(&graph, "secret:a");
898 assert_eq!(plan.total_secrets, 3); }
900
901 #[test]
902 fn test_rotation_critical_first() {
903 let graph = test_graph();
904 add_weighted_dependency(
905 &graph,
906 "secret:a",
907 "secret:low",
908 DependencyWeight::Low,
909 None,
910 1000,
911 )
912 .unwrap();
913 add_weighted_dependency(
914 &graph,
915 "secret:a",
916 "secret:critical",
917 DependencyWeight::Critical,
918 None,
919 2000,
920 )
921 .unwrap();
922
923 let plan = rotation_plan(&graph, "secret:a");
924 assert_eq!(plan.rotation_order.len(), 2);
925 assert_eq!(plan.rotation_order[0].secret, "secret:critical");
927 }
928}