Skip to main content

zoom_cli/commands/
users.rs

1use crate::api::types::{CreateUserInfo, CreateUserRequest};
2use crate::api::{ApiError, ZoomClient};
3use crate::output::{self, OutputConfig};
4
5pub async fn list(
6    client: &mut ZoomClient,
7    out: &OutputConfig,
8    status: Option<&str>,
9) -> Result<(), ApiError> {
10    let result = client.list_users(status).await?;
11
12    if out.json {
13        out.print_data(&serde_json::to_string_pretty(&result).expect("serialize"));
14    } else {
15        if result.users.is_empty() {
16            out.print_message("No users found.");
17            return Ok(());
18        }
19        let rows: Vec<Vec<String>> = result
20            .users
21            .iter()
22            .map(|u| {
23                vec![
24                    u.id.clone(),
25                    u.email.clone(),
26                    u.display_name.clone().unwrap_or_default(),
27                    u.status.clone().unwrap_or_default(),
28                ]
29            })
30            .collect();
31        out.print_data(&output::table(&["ID", "EMAIL", "NAME", "STATUS"], &rows));
32        if let Some(total) = result.total_records {
33            out.print_message(&format!("{total} user(s) total"));
34        }
35    }
36    Ok(())
37}
38
39pub async fn get(
40    client: &mut ZoomClient,
41    out: &OutputConfig,
42    id_or_email: &str,
43) -> Result<(), ApiError> {
44    let user = client.get_user(id_or_email).await?;
45
46    if out.json {
47        out.print_data(&serde_json::to_string_pretty(&user).expect("serialize"));
48    } else {
49        let name = user
50            .display_name
51            .clone()
52            .or_else(|| match (&user.first_name, &user.last_name) {
53                (Some(f), Some(l)) => Some(format!("{f} {l}")),
54                (Some(f), None) => Some(f.clone()),
55                _ => None,
56            })
57            .unwrap_or_default();
58        out.print_data(&output::kv_block(&[
59            ("id", user.id.clone()),
60            ("email", user.email.clone()),
61            ("name", name),
62            ("status", user.status.clone().unwrap_or_default()),
63        ]));
64    }
65    Ok(())
66}
67
68pub async fn me(client: &mut ZoomClient, out: &OutputConfig) -> Result<(), ApiError> {
69    get(client, out, "me").await
70}
71
72pub async fn create(
73    client: &mut ZoomClient,
74    out: &OutputConfig,
75    email: String,
76    first_name: Option<String>,
77    last_name: Option<String>,
78    user_type: u8,
79) -> Result<(), ApiError> {
80    let req = CreateUserRequest {
81        action: "create".into(),
82        user_info: CreateUserInfo {
83            email,
84            user_type,
85            first_name,
86            last_name,
87        },
88    };
89    let user = client.create_user(req).await?;
90    if out.json {
91        out.print_data(&serde_json::to_string_pretty(&user).expect("serialize"));
92    } else {
93        out.print_message(&format!("Created user: {} ({})", user.email, user.id));
94    }
95    Ok(())
96}
97
98pub async fn deactivate(
99    client: &mut ZoomClient,
100    out: &OutputConfig,
101    id_or_email: &str,
102) -> Result<(), ApiError> {
103    client.set_user_status(id_or_email, "deactivate").await?;
104    out.print_message(&format!("User {} deactivated.", id_or_email));
105    Ok(())
106}
107
108pub async fn activate(
109    client: &mut ZoomClient,
110    out: &OutputConfig,
111    id_or_email: &str,
112) -> Result<(), ApiError> {
113    client.set_user_status(id_or_email, "activate").await?;
114    out.print_message(&format!("User {} activated.", id_or_email));
115    Ok(())
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::api::ZoomClient;
122    use wiremock::matchers::{method, path};
123    use wiremock::{Mock, MockServer, ResponseTemplate};
124
125    fn test_out_json() -> OutputConfig {
126        OutputConfig::for_test()
127    }
128
129    #[tokio::test]
130    async fn users_list_json_includes_total_records() {
131        let server = MockServer::start().await;
132        Mock::given(method("GET"))
133            .and(path("/v2/users"))
134            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
135                "users": [
136                    {"id": "u1", "email": "a@test.com", "display_name": "Alice", "status": "active"}
137                ],
138                "total_records": 1
139            })))
140            .mount(&server)
141            .await;
142
143        let mut client =
144            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
145        list(&mut client, &test_out_json(), None).await.unwrap();
146    }
147
148    #[tokio::test]
149    async fn users_get_returns_not_found_for_unknown_user() {
150        let server = MockServer::start().await;
151        Mock::given(method("GET"))
152            .and(path("/v2/users/nobody@example.com"))
153            .respond_with(ResponseTemplate::new(404).set_body_string("User not found"))
154            .mount(&server)
155            .await;
156
157        let mut client =
158            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
159        let err = get(&mut client, &test_out_json(), "nobody@example.com")
160            .await
161            .unwrap_err();
162        assert!(matches!(err, ApiError::NotFound(_)));
163    }
164
165    #[tokio::test]
166    async fn users_me_delegates_to_get_with_me() {
167        let server = MockServer::start().await;
168        Mock::given(method("GET"))
169            .and(path("/v2/users/me"))
170            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
171                "id": "self-user", "email": "me@example.com", "display_name": "Me"
172            })))
173            .mount(&server)
174            .await;
175
176        let mut client =
177            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
178        me(&mut client, &test_out_json()).await.unwrap();
179    }
180
181    #[tokio::test]
182    async fn users_create_returns_created_user() {
183        let server = MockServer::start().await;
184        Mock::given(method("POST"))
185            .and(path("/v2/users"))
186            .and(wiremock::matchers::body_json(serde_json::json!({
187                "action": "create",
188                "user_info": {
189                    "email": "jane@example.com",
190                    "type": 1,
191                    "first_name": "Jane",
192                    "last_name": "Doe"
193                }
194            })))
195            .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
196                "id": "new-user-id",
197                "email": "jane@example.com",
198                "first_name": "Jane",
199                "last_name": "Doe",
200                "status": "active"
201            })))
202            .mount(&server)
203            .await;
204
205        let mut client =
206            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
207        create(
208            &mut client,
209            &test_out_json(),
210            "jane@example.com".into(),
211            Some("Jane".into()),
212            Some("Doe".into()),
213            1,
214        )
215        .await
216        .unwrap();
217    }
218
219    #[tokio::test]
220    async fn users_deactivate_sends_correct_action() {
221        let server = MockServer::start().await;
222        Mock::given(method("PUT"))
223            .and(path("/v2/users/u1/status"))
224            .and(wiremock::matchers::body_json(
225                serde_json::json!({"action": "deactivate"}),
226            ))
227            .respond_with(ResponseTemplate::new(204))
228            .mount(&server)
229            .await;
230
231        let mut client =
232            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
233        deactivate(&mut client, &test_out_json(), "u1")
234            .await
235            .unwrap();
236    }
237
238    #[tokio::test]
239    async fn users_activate_sends_correct_action() {
240        let server = MockServer::start().await;
241        Mock::given(method("PUT"))
242            .and(path("/v2/users/u1/status"))
243            .and(wiremock::matchers::body_json(
244                serde_json::json!({"action": "activate"}),
245            ))
246            .respond_with(ResponseTemplate::new(204))
247            .mount(&server)
248            .await;
249
250        let mut client =
251            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
252        activate(&mut client, &test_out_json(), "u1").await.unwrap();
253    }
254}