use super::*;
use crate::core::config::{TeamConfig, TeamMember};
use std::collections::HashMap;
fn make_team() -> TeamConfig {
let mut aliases = HashMap::new();
aliases.insert("bobby".into(), "Bob Smith".into());
TeamConfig {
members: vec![TeamMember {
name: "Bob Smith".into(),
email: "bob@example.com".into(),
aliases: vec!["bsmith@example.com".into()],
}],
aliases,
canonical_domain: None,
}
}
#[test]
fn exact_email_alias_match() {
let r = IdentityResolver::new(Some(&make_team()));
let (n, e) = r.resolve("Whoever", "bsmith@example.com");
assert_eq!(n, "Bob Smith");
assert_eq!(e, "bob@example.com");
}
#[test]
fn exact_name_alias_match() {
let r = IdentityResolver::new(Some(&make_team()));
let (n, e) = r.resolve("bobby", "x@y.com");
assert_eq!(n, "Bob Smith");
assert_eq!(e, "bob@example.com");
}
#[test]
fn fuzzy_match_canonical_name() {
let r = IdentityResolver::new(Some(&make_team()));
let (n, _e) = r.resolve("Bob Smyth", "unknown@elsewhere.com");
assert_eq!(n, "Bob Smith");
}
#[test]
fn no_match_returns_input() {
let r = IdentityResolver::new(Some(&make_team()));
let (n, e) = r.resolve("Zelda Q", "zelda@nowhere.test");
assert_eq!(n, "Zelda Q");
assert_eq!(e, "zelda@nowhere.test");
}
#[test]
fn empty_team_passthrough() {
let r = IdentityResolver::new(None);
let (n, e) = r.resolve("Anyone", "anyone@x.com");
assert_eq!(n, "Anyone");
assert_eq!(e, "anyone@x.com");
}
#[test]
fn all_aliases_registered() {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
map.insert(
"Alice Smith".to_string(),
vec![
"alice@company.com".into(),
"alice.smith@personal.com".into(),
"asmith".into(), ],
);
let r = IdentityResolver::from_alias_map(&map);
let (n, e) = r.resolve("whoever", "alice@company.com");
assert_eq!(n, "Alice Smith");
assert_eq!(e, "alice@company.com");
let (n, e) = r.resolve("whoever", "alice.smith@personal.com");
assert_eq!(n, "Alice Smith");
assert_eq!(e, "alice@company.com");
let (n, e) = r.resolve("asmith", "noise@nowhere.test");
assert_eq!(n, "Alice Smith");
assert_eq!(e, "alice@company.com");
}
#[test]
fn email_local_part_fuzzy_match() {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
map.insert(
"Bob Matsuoka".to_string(),
vec!["bob.matsuoka@duettoresearch.com".into()],
);
let r = IdentityResolver::from_alias_map(&map);
let (n, e) = r.resolve("Bob M", "bob.matsuoka@otherdomain.com");
assert_eq!(n, "Bob Matsuoka");
assert_eq!(e, "bob.matsuoka@duettoresearch.com");
}
#[test]
fn case_insensitive_email_lookup() {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
map.insert("Alice Smith".to_string(), vec!["alice@company.com".into()]);
let r = IdentityResolver::from_alias_map(&map);
let (n, e) = r.resolve("Whoever", "ALICE@COMPANY.COM");
assert_eq!(n, "Alice Smith");
assert_eq!(e, "alice@company.com");
let (n2, e2) = r.resolve("WhoEver", "Alice@Company.Com");
assert_eq!(n2, "Alice Smith");
assert_eq!(e2, "alice@company.com");
}
#[test]
fn short_name_fuzzy() {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
map.insert(
"Bob Matsuoka".to_string(),
vec!["bob.matsuoka@co.com".into()],
);
let r = IdentityResolver::from_alias_map(&map);
let (n, _e) = r.resolve("Bob M", "bobm@unknown.test");
assert_eq!(n, "Bob Matsuoka");
}
#[test]
fn unknown_author_passthrough() {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
map.insert("Alice Smith".to_string(), vec!["alice@company.com".into()]);
let r = IdentityResolver::from_alias_map(&map);
let (n, e) = r.resolve("Zelda Q", "zelda@nowhere.test");
assert_eq!(n, "Zelda Q");
assert_eq!(e, "zelda@nowhere.test");
}
#[test]
fn multiple_emails_same_person() {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
map.insert(
"Andre Ramos".to_string(),
vec![
"andre.ramos@duettoresearch.com".into(),
"129991831+andreramosduetto@users.noreply.github.com".into(),
"andre@personal.dev".into(),
],
);
let r = IdentityResolver::from_alias_map(&map);
let (n1, e1) = r.resolve("Andre Ramos", "andre.ramos@duettoresearch.com");
let (n2, e2) = r.resolve(
"andreramosduetto",
"129991831+andreramosduetto@users.noreply.github.com",
);
let (n3, e3) = r.resolve("A. Ramos", "andre@personal.dev");
assert_eq!(n1, "Andre Ramos");
assert_eq!(n2, "Andre Ramos");
assert_eq!(n3, "Andre Ramos");
assert_eq!(e1, "andre.ramos@duettoresearch.com");
assert_eq!(e2, "andre.ramos@duettoresearch.com");
assert_eq!(e3, "andre.ramos@duettoresearch.com");
}
#[test]
fn duetto_contractors_config_resolves() {
let unique = format!(
"tga-duetto-contractors-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let tmp = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&tmp).expect("create tmp");
let aliases_yaml = r#"
developers:
- name: "Andre Ramos"
primary_email: "andre.ramos@duettoresearch.com"
aliases:
- "129991831+andreramosduetto@users.noreply.github.com"
- name: "Akash Arora"
primary_email: "akash.arora@duettoresearch.com"
aliases:
- "Akash.Arora-c@duettoresearch.com"
- "akash-duetto"
- name: "Janga Vinod Kumar Reddy"
primary_email: "janga.reddy@duettoresearch.com"
aliases:
- "jangareddy-duetto"
- "164324948+jangareddy-duetto@users.noreply.github.com"
"#;
let aliases_path = tmp.join("aliases.yaml");
std::fs::write(&aliases_path, aliases_yaml).expect("write aliases");
let config_yaml = format!(
"version: \"1.0\"\naliases_file: \"{}\"\n",
aliases_path.to_string_lossy()
);
let config_path = tmp.join("duetto-contractors.yaml");
std::fs::write(&config_path, config_yaml).expect("write config");
let cfg =
crate::core::config::Config::load(&config_path).expect("load duetto-contractors yaml");
let r = IdentityResolver::from_config(&cfg);
let (n, _) = r.resolve("whoever", "andre.ramos@duettoresearch.com");
assert_eq!(n, "Andre Ramos");
let (n, _) = r.resolve("whoever", "Akash.Arora-c@duettoresearch.com");
assert_eq!(n, "Akash Arora");
let (n, _) = r.resolve("jangareddy-duetto", "noise@nowhere.test");
assert_eq!(n, "Janga Vinod Kumar Reddy");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn normalize_for_fuzzy_basic() {
assert_eq!(normalize_for_fuzzy("Bob.Matsuoka"), "bob matsuoka");
assert_eq!(normalize_for_fuzzy("alice_smith-c"), "alice smith c");
assert_eq!(normalize_for_fuzzy(" Foo Bar "), "foo bar");
}
#[test]
fn email_local_part_basic() {
assert_eq!(email_local_part("Bob@Example.COM"), "bob");
assert_eq!(email_local_part("no-at-symbol"), "no-at-symbol");
}
#[test]
fn email_domain_matches_basic() {
assert!(email_domain_matches(
"a@DUETTORESEARCH.COM",
"duettoresearch.com"
));
assert!(email_domain_matches(
"a@duettoresearch.com",
"@duettoresearch.com"
));
assert!(!email_domain_matches("a@other.com", "duettoresearch.com"));
assert!(!email_domain_matches("invalid-email", "duettoresearch.com"));
assert!(!email_domain_matches("a@duettoresearch.com", ""));
}
#[test]
fn canonical_domain_prefers_org_email_for_team_member() {
let team = TeamConfig {
members: vec![TeamMember {
name: "Alice Org".into(),
email: "alice@duettoresearch.com".into(),
aliases: vec!["alice@personal.com".into()],
}],
aliases: HashMap::new(),
canonical_domain: Some("duettoresearch.com".into()),
};
let r = IdentityResolver::new(Some(&team));
let (_, e) = r.resolve("Alice Org", "alice@personal.com");
assert_eq!(e, "alice@duettoresearch.com");
assert_eq!(r.canonical_domain(), Some("duettoresearch.com"));
}
#[test]
fn canonical_domain_routes_new_personal_email_to_existing_org_row() {
use crate::core::db::Database;
use rusqlite::params;
let team = TeamConfig {
members: vec![],
aliases: HashMap::new(),
canonical_domain: Some("duettoresearch.com".into()),
};
let r = IdentityResolver::new(Some(&team));
let db = Database::open_in_memory().expect("db");
let _ = r
.upsert_author(&db, "Bob Matsuoka", "bob@duettoresearch.com")
.expect("seed");
let id = r
.upsert_author(&db, "Bob Matsuoka", "bob@personal.com")
.expect("upsert");
let stored_email: String = db
.connection()
.query_row(
"SELECT canonical_email FROM authors WHERE id = ?1",
params![id],
|row| row.get(0),
)
.expect("lookup");
assert_eq!(stored_email, "bob@duettoresearch.com");
let count: i64 = db
.connection()
.query_row(
"SELECT COUNT(*) FROM authors WHERE canonical_name = 'Bob Matsuoka'",
[],
|row| row.get(0),
)
.expect("count");
assert_eq!(count, 1);
}
#[test]
fn canonical_domain_absent_falls_back_to_first_seen_email() {
use crate::core::db::Database;
let r = IdentityResolver::new(None);
assert_eq!(r.canonical_domain(), None);
let db = Database::open_in_memory().expect("db");
let _ = r
.upsert_author(&db, "Carol", "carol@personal.com")
.expect("seed");
let _ = r
.upsert_author(&db, "Carol", "carol@work.com")
.expect("upsert");
let count: i64 = db
.connection()
.query_row(
"SELECT COUNT(*) FROM authors WHERE canonical_name = 'Carol'",
[],
|row| row.get(0),
)
.expect("count");
assert_eq!(count, 2);
}
#[test]
fn canonical_domain_read_from_config() {
let yaml = r#"
team:
canonical_domain: "duettoresearch.com"
members:
- name: "Alice"
email: "alice@duettoresearch.com"
"#;
let cfg: crate::core::config::Config = serde_yaml::from_str(yaml).expect("parse");
let r = IdentityResolver::from_config(&cfg);
assert_eq!(r.canonical_domain(), Some("duettoresearch.com"));
}