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