use devboy_secret_patterns::{Catalogue, RotationMethodSpec, SecretPattern};
use crate::index::{IndexEntry, RotationMethod};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InheritanceWarning {
pub kind: InheritanceWarningKind,
pub pattern_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InheritanceWarningKind {
UnknownPatternId,
}
pub fn apply_pattern_inheritance(
entry: &IndexEntry,
catalogue: &Catalogue,
) -> (IndexEntry, Option<InheritanceWarning>) {
let resolved = entry.clone();
let Some(id) = entry.pattern_id.as_deref() else {
return (resolved, None);
};
let Some(pattern) = catalogue.find(id) else {
return (
resolved,
Some(InheritanceWarning {
kind: InheritanceWarningKind::UnknownPatternId,
pattern_id: id.to_owned(),
}),
);
};
let resolved = inherit_from_pattern(resolved, pattern);
(resolved, None)
}
fn inherit_from_pattern(mut entry: IndexEntry, pattern: &dyn SecretPattern) -> IndexEntry {
if entry.format_regex.is_none() {
entry.format_regex = Some(pattern.format_regex().as_str().to_owned());
}
if let Some(meta) = pattern.metadata() {
if entry.retrieval_url.is_none() {
entry.retrieval_url = Some(meta.retrieval_url_template.to_string());
}
if entry.rotate_every_days.is_none() {
if let Some(d) = meta.default_expiry_days {
entry.rotate_every_days = Some(d);
}
}
}
if let Some(rotation) = pattern.rotation() {
if entry.rotation_method.is_none() {
entry.rotation_method = Some(map_rotation_method(&rotation.method));
}
}
entry
}
fn map_rotation_method(m: &RotationMethodSpec) -> RotationMethod {
match m {
RotationMethodSpec::Manual => RotationMethod::Manual,
RotationMethodSpec::ProviderUi { .. } => RotationMethod::ProviderUi,
RotationMethodSpec::ProviderApi => RotationMethod::ProviderApi,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::index::{Gate, IndexEntry, RotationMethod};
fn entry_with_pattern(id: &str) -> IndexEntry {
IndexEntry {
pattern_id: Some(id.to_owned()),
..IndexEntry::default()
}
}
#[test]
fn entry_without_pattern_id_passes_through_unchanged() {
let cat = Catalogue::builtins_only();
let entry = IndexEntry {
description: Some("explicit".to_owned()),
..IndexEntry::default()
};
let (resolved, warning) = apply_pattern_inheritance(&entry, &cat);
assert_eq!(resolved, entry);
assert!(warning.is_none());
}
#[test]
fn unknown_pattern_id_returns_entry_and_warning() {
let cat = Catalogue::builtins_only();
let entry = entry_with_pattern("no-such-pattern");
let (resolved, warning) = apply_pattern_inheritance(&entry, &cat);
assert_eq!(resolved, entry, "entry must be returned unchanged");
let w = warning.expect("must produce a warning");
assert_eq!(w.kind, InheritanceWarningKind::UnknownPatternId);
assert_eq!(w.pattern_id, "no-such-pattern");
}
#[test]
fn known_pattern_inherits_format_regex() {
let cat = Catalogue::builtins_only();
let entry = entry_with_pattern("github-pat");
let (resolved, warning) = apply_pattern_inheritance(&entry, &cat);
assert!(warning.is_none());
let regex = resolved.format_regex.expect("regex inherited");
assert!(regex.starts_with('^'));
assert!(regex.contains("gh"));
}
#[test]
fn explicit_format_regex_is_not_overridden() {
let cat = Catalogue::builtins_only();
let entry = IndexEntry {
pattern_id: Some("github-pat".to_owned()),
format_regex: Some("^my-explicit-regex$".to_owned()),
..IndexEntry::default()
};
let (resolved, _) = apply_pattern_inheritance(&entry, &cat);
assert_eq!(
resolved.format_regex.as_deref(),
Some("^my-explicit-regex$")
);
}
#[test]
fn known_pattern_inherits_retrieval_url() {
let cat = Catalogue::builtins_only();
let entry = entry_with_pattern("github-pat");
let (resolved, _) = apply_pattern_inheritance(&entry, &cat);
assert_eq!(
resolved.retrieval_url.as_deref(),
Some("https://github.com/settings/tokens")
);
}
#[test]
fn explicit_retrieval_url_is_not_overridden() {
let cat = Catalogue::builtins_only();
let entry = IndexEntry {
pattern_id: Some("github-pat".to_owned()),
retrieval_url: Some("https://internal.example/tokens".to_owned()),
..IndexEntry::default()
};
let (resolved, _) = apply_pattern_inheritance(&entry, &cat);
assert_eq!(
resolved.retrieval_url.as_deref(),
Some("https://internal.example/tokens")
);
}
#[test]
fn known_pattern_inherits_rotate_every_days() {
let cat = Catalogue::builtins_only();
let entry = entry_with_pattern("github-pat");
let (resolved, _) = apply_pattern_inheritance(&entry, &cat);
assert_eq!(resolved.rotate_every_days, Some(90));
}
#[test]
fn explicit_rotate_every_days_is_not_overridden() {
let cat = Catalogue::builtins_only();
let entry = IndexEntry {
pattern_id: Some("github-pat".to_owned()),
rotate_every_days: Some(30),
..IndexEntry::default()
};
let (resolved, _) = apply_pattern_inheritance(&entry, &cat);
assert_eq!(resolved.rotate_every_days, Some(30));
}
#[test]
fn pattern_without_metadata_only_inherits_regex() {
let cat = Catalogue::builtins_only();
let entry = entry_with_pattern("jwt");
let (resolved, _) = apply_pattern_inheritance(&entry, &cat);
assert!(resolved.format_regex.is_some(), "regex must inherit");
assert!(
resolved.retrieval_url.is_none(),
"no metadata → no retrieval url"
);
assert!(
resolved.rotate_every_days.is_none(),
"no metadata → no expiry default"
);
}
#[test]
fn unrelated_fields_pass_through_unchanged() {
let cat = Catalogue::builtins_only();
let entry = IndexEntry {
pattern_id: Some("github-pat".to_owned()),
description: Some("My deploy token".to_owned()),
default_gate: Some(Gate::Touchid),
expires_at: Some("2026-08-01".to_owned()),
last_rotated_at: Some("2026-05-02".to_owned()),
required_scopes: vec!["repo".to_owned()],
env_var: Some("GH_TOKEN".to_owned()),
cache_ttl_seconds_max: Some(60),
..IndexEntry::default()
};
let (resolved, _) = apply_pattern_inheritance(&entry, &cat);
assert!(resolved.format_regex.is_some());
assert!(resolved.retrieval_url.is_some());
assert_eq!(resolved.rotate_every_days, Some(90));
assert_eq!(resolved.description.as_deref(), Some("My deploy token"));
assert_eq!(resolved.default_gate, Some(Gate::Touchid));
assert_eq!(resolved.expires_at.as_deref(), Some("2026-08-01"));
assert_eq!(resolved.last_rotated_at.as_deref(), Some("2026-05-02"));
assert_eq!(resolved.required_scopes, vec!["repo"]);
assert_eq!(resolved.env_var.as_deref(), Some("GH_TOKEN"));
assert_eq!(resolved.cache_ttl_seconds_max, Some(60));
}
#[test]
fn rotation_method_remains_none_for_v1_builtins() {
let cat = Catalogue::builtins_only();
let entry = entry_with_pattern("github-pat");
let (resolved, _) = apply_pattern_inheritance(&entry, &cat);
assert_eq!(
resolved.rotation_method, None,
"no built-in supplies a rotation spec yet"
);
}
#[test]
fn map_rotation_method_covers_each_variant() {
assert_eq!(
map_rotation_method(&RotationMethodSpec::Manual),
RotationMethod::Manual
);
assert_eq!(
map_rotation_method(&RotationMethodSpec::ProviderUi {
url_template: "https://example/r"
}),
RotationMethod::ProviderUi
);
assert_eq!(
map_rotation_method(&RotationMethodSpec::ProviderApi),
RotationMethod::ProviderApi
);
}
}