Skip to main content

osp_cli/
api.rs

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}