1use std::collections::HashMap;
2
3use crate::core::row::Row;
4use crate::ports::{LdapDirectory, apply_filter_and_projection};
5use anyhow::Result;
6use serde_json::json;
7
8#[derive(Debug, Clone)]
9pub struct MockLdapClient {
10 users: HashMap<String, Row>,
11 netgroups: HashMap<String, Row>,
12}
13
14impl Default for MockLdapClient {
15 fn default() -> Self {
16 let mut users = HashMap::new();
17 users.insert(
18 "oistes".to_string(),
19 json!({
20 "uid": "oistes",
21 "cn": "Øistein Søvik",
22 "uidNumber": "361000",
23 "gidNumber": "346297",
24 "homeDirectory": "/uio/kant/usit-gsd-u1/oistes",
25 "loginShell": "/local/gnu/bin/bash",
26 "objectClass": ["uioMembership", "top", "account", "posixAccount"],
27 "eduPersonAffiliation": ["employee", "member", "staff"],
28 "uioAffiliation": "ANSATT@373034",
29 "uioPrimaryAffiliation": "ANSATT@373034",
30 "netgroups": ["ucore", "usit", "it-uio-azure-users"],
31 "filegroups": ["oistes", "ucore", "usit"]
32 })
33 .as_object()
34 .cloned()
35 .expect("static user fixture must be object"),
36 );
37
38 let mut netgroups = HashMap::new();
39 netgroups.insert(
40 "ucore".to_string(),
41 json!({
42 "cn": "ucore",
43 "description": "Kjernen av Unix-grupp på USIT",
44 "objectClass": ["top", "nisNetgroup"],
45 "members": [
46 "andreasd",
47 "arildlj",
48 "kjetilk",
49 "oistes",
50 "trondham",
51 "werner"
52 ]
53 })
54 .as_object()
55 .cloned()
56 .expect("static netgroup fixture must be object"),
57 );
58
59 Self { users, netgroups }
60 }
61}
62
63impl LdapDirectory for MockLdapClient {
64 fn user(
65 &self,
66 uid: &str,
67 filter: Option<&str>,
68 attributes: Option<&[String]>,
69 ) -> Result<Vec<Row>> {
70 let raw_rows = if uid.contains('*') {
71 self.users
72 .iter()
73 .filter(|(key, _)| wildcard_match(uid, key))
74 .map(|(_, row)| row.clone())
75 .collect::<Vec<Row>>()
76 } else {
77 self.users
78 .get(uid)
79 .cloned()
80 .map(|row| vec![row])
81 .unwrap_or_default()
82 };
83
84 Ok(apply_filter_and_projection(raw_rows, filter, attributes))
85 }
86
87 fn netgroup(
88 &self,
89 name: &str,
90 filter: Option<&str>,
91 attributes: Option<&[String]>,
92 ) -> Result<Vec<Row>> {
93 let raw_rows = if name.contains('*') {
94 self.netgroups
95 .iter()
96 .filter(|(key, _)| wildcard_match(name, key))
97 .map(|(_, row)| row.clone())
98 .collect::<Vec<Row>>()
99 } else {
100 self.netgroups
101 .get(name)
102 .cloned()
103 .map(|row| vec![row])
104 .unwrap_or_default()
105 };
106
107 Ok(apply_filter_and_projection(raw_rows, filter, attributes))
108 }
109}
110
111fn wildcard_match(pattern: &str, value: &str) -> bool {
112 let escaped = regex::escape(pattern).replace("\\*", ".*");
113 let re = regex::Regex::new(&format!("^{escaped}$"))
114 .expect("escaped wildcard patterns must compile as regexes");
115 re.is_match(value)
116}
117
118#[cfg(test)]
119mod tests {
120 use crate::ports::LdapDirectory;
121
122 use super::MockLdapClient;
123
124 #[test]
125 fn user_filter_uid_equals_returns_match() {
126 let ldap = MockLdapClient::default();
127 let rows = ldap
128 .user("oistes", Some("uid=oistes"), None)
129 .expect("query should succeed");
130 assert_eq!(rows.len(), 1);
131 }
132
133 #[test]
134 fn wildcard_queries_match_users_and_netgroups() {
135 let ldap = MockLdapClient::default();
136
137 let users = ldap.user("oi*", None, None).expect("query should succeed");
138 assert_eq!(users.len(), 1);
139 assert_eq!(
140 users[0].get("uid").and_then(|value| value.as_str()),
141 Some("oistes")
142 );
143
144 let netgroups = ldap
145 .netgroup("u*", None, Some(&["cn".to_string()]))
146 .expect("query should succeed");
147 assert_eq!(netgroups.len(), 1);
148 assert_eq!(
149 netgroups[0].get("cn").and_then(|value| value.as_str()),
150 Some("ucore")
151 );
152 assert_eq!(netgroups[0].len(), 1);
153 }
154
155 #[test]
156 fn missing_entries_return_empty_results() {
157 let ldap = MockLdapClient::default();
158
159 let users = ldap
160 .user("does-not-exist", Some("uid=does-not-exist"), None)
161 .expect("query should succeed");
162 assert!(users.is_empty());
163
164 let netgroups = ldap
165 .netgroup("nope*", None, None)
166 .expect("query should succeed");
167 assert!(netgroups.is_empty());
168 }
169
170 #[test]
171 fn exact_netgroup_queries_return_single_match() {
172 let ldap = MockLdapClient::default();
173
174 let netgroups = ldap
175 .netgroup("ucore", None, Some(&["cn".to_string()]))
176 .expect("query should succeed");
177
178 assert_eq!(netgroups.len(), 1);
179 assert_eq!(
180 netgroups[0].get("cn").and_then(|value| value.as_str()),
181 Some("ucore")
182 );
183 }
184}