1pub mod auth;
8pub mod graph_client;
9pub mod types;
10
11use async_trait::async_trait;
12use serde_json::json;
13use std::sync::Arc;
14use zeroclaw_api::tool::{Tool, ToolResult};
15use zeroclaw_config::policy::SecurityPolicy;
16use zeroclaw_config::policy::ToolOperation;
17
18const MAX_ONEDRIVE_DOWNLOAD_SIZE: usize = 10 * 1024 * 1024;
20
21const DEFAULT_TOP: u32 = 25;
23
24pub struct Microsoft365Tool {
25 config: types::Microsoft365ResolvedConfig,
26 security: Arc<SecurityPolicy>,
27 token_cache: Arc<auth::TokenCache>,
28 http_client: reqwest::Client,
29}
30
31impl Microsoft365Tool {
32 pub fn new(
33 config: types::Microsoft365ResolvedConfig,
34 security: Arc<SecurityPolicy>,
35 zeroclaw_dir: &std::path::Path,
36 ) -> anyhow::Result<Self> {
37 let http_client = zeroclaw_config::schema::build_runtime_proxy_client_with_timeouts(
38 "tool.microsoft365",
39 60,
40 10,
41 );
42 let token_cache = Arc::new(auth::TokenCache::new(config.clone(), zeroclaw_dir)?);
43 Ok(Self {
44 config,
45 security,
46 token_cache,
47 http_client,
48 })
49 }
50
51 async fn get_token(&self) -> anyhow::Result<String> {
52 self.token_cache.get_token(&self.http_client).await
53 }
54
55 fn user_id(&self) -> &str {
56 &self.config.user_id
57 }
58
59 async fn dispatch(&self, action: &str, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
60 match action {
61 "mail_list" => self.handle_mail_list(args).await,
62 "mail_send" => self.handle_mail_send(args).await,
63 "teams_message_list" => self.handle_teams_message_list(args).await,
64 "teams_message_send" => self.handle_teams_message_send(args).await,
65 "calendar_events_list" => self.handle_calendar_events_list(args).await,
66 "calendar_event_create" => self.handle_calendar_event_create(args).await,
67 "calendar_event_delete" => self.handle_calendar_event_delete(args).await,
68 "onedrive_list" => self.handle_onedrive_list(args).await,
69 "onedrive_download" => self.handle_onedrive_download(args).await,
70 "sharepoint_search" => self.handle_sharepoint_search(args).await,
71 _ => Ok(ToolResult {
72 success: false,
73 output: String::new(),
74 error: Some(format!("Unknown action: {action}")),
75 }),
76 }
77 }
78
79 async fn handle_mail_list(&self, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
82 self.security
83 .enforce_tool_operation(ToolOperation::Read, "microsoft365.mail_list")
84 .map_err(|e| {
85 ::zeroclaw_log::record!(
86 ERROR,
87 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
88 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
89 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
90 "microsoft365: tool operation denied by policy"
91 );
92 anyhow::Error::msg(e.to_string())
93 })?;
94
95 let token = self.get_token().await?;
96 let folder = args["folder"].as_str();
97 let top = u32::try_from(args["top"].as_u64().unwrap_or(u64::from(DEFAULT_TOP)))
98 .unwrap_or(DEFAULT_TOP);
99
100 let result =
101 graph_client::mail_list(&self.http_client, &token, self.user_id(), folder, top).await?;
102
103 Ok(ToolResult {
104 success: true,
105 output: serde_json::to_string_pretty(&result)?,
106 error: None,
107 })
108 }
109
110 async fn handle_teams_message_list(
111 &self,
112 args: &serde_json::Value,
113 ) -> anyhow::Result<ToolResult> {
114 self.security
115 .enforce_tool_operation(ToolOperation::Read, "microsoft365.teams_message_list")
116 .map_err(|e| {
117 ::zeroclaw_log::record!(
118 ERROR,
119 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
120 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
121 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
122 "microsoft365: tool operation denied by policy"
123 );
124 anyhow::Error::msg(e.to_string())
125 })?;
126
127 let token = self.get_token().await?;
128 let team_id = args["team_id"].as_str().ok_or_else(|| {
129 ::zeroclaw_log::record!(
130 WARN,
131 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
132 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
133 "mod: team_id is required"
134 );
135 anyhow::Error::msg("team_id is required")
136 })?;
137 let channel_id = args["channel_id"].as_str().ok_or_else(|| {
138 ::zeroclaw_log::record!(
139 WARN,
140 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
141 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
142 "mod: channel_id is required"
143 );
144 anyhow::Error::msg("channel_id is required")
145 })?;
146 let top = u32::try_from(args["top"].as_u64().unwrap_or(u64::from(DEFAULT_TOP)))
147 .unwrap_or(DEFAULT_TOP);
148
149 let result =
150 graph_client::teams_message_list(&self.http_client, &token, team_id, channel_id, top)
151 .await?;
152
153 Ok(ToolResult {
154 success: true,
155 output: serde_json::to_string_pretty(&result)?,
156 error: None,
157 })
158 }
159
160 async fn handle_calendar_events_list(
161 &self,
162 args: &serde_json::Value,
163 ) -> anyhow::Result<ToolResult> {
164 self.security
165 .enforce_tool_operation(ToolOperation::Read, "microsoft365.calendar_events_list")
166 .map_err(|e| {
167 ::zeroclaw_log::record!(
168 ERROR,
169 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
170 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
171 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
172 "microsoft365: tool operation denied by policy"
173 );
174 anyhow::Error::msg(e.to_string())
175 })?;
176
177 let token = self.get_token().await?;
178 let start = args["start"].as_str().ok_or_else(|| {
179 ::zeroclaw_log::record!(
180 WARN,
181 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
182 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
183 "mod: start datetime is required"
184 );
185 anyhow::Error::msg("start datetime is required")
186 })?;
187 let end = args["end"].as_str().ok_or_else(|| {
188 ::zeroclaw_log::record!(
189 WARN,
190 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
191 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
192 "mod: end datetime is required"
193 );
194 anyhow::Error::msg("end datetime is required")
195 })?;
196 let top = u32::try_from(args["top"].as_u64().unwrap_or(u64::from(DEFAULT_TOP)))
197 .unwrap_or(DEFAULT_TOP);
198
199 let result = graph_client::calendar_events_list(
200 &self.http_client,
201 &token,
202 self.user_id(),
203 start,
204 end,
205 top,
206 )
207 .await?;
208
209 Ok(ToolResult {
210 success: true,
211 output: serde_json::to_string_pretty(&result)?,
212 error: None,
213 })
214 }
215
216 async fn handle_onedrive_list(&self, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
217 self.security
218 .enforce_tool_operation(ToolOperation::Read, "microsoft365.onedrive_list")
219 .map_err(|e| {
220 ::zeroclaw_log::record!(
221 ERROR,
222 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
223 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
224 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
225 "microsoft365: tool operation denied by policy"
226 );
227 anyhow::Error::msg(e.to_string())
228 })?;
229
230 let token = self.get_token().await?;
231 let path = args["path"].as_str();
232
233 let result =
234 graph_client::onedrive_list(&self.http_client, &token, self.user_id(), path).await?;
235
236 Ok(ToolResult {
237 success: true,
238 output: serde_json::to_string_pretty(&result)?,
239 error: None,
240 })
241 }
242
243 async fn handle_onedrive_download(
244 &self,
245 args: &serde_json::Value,
246 ) -> anyhow::Result<ToolResult> {
247 self.security
248 .enforce_tool_operation(ToolOperation::Read, "microsoft365.onedrive_download")
249 .map_err(|e| {
250 ::zeroclaw_log::record!(
251 ERROR,
252 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
253 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
254 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
255 "microsoft365: tool operation denied by policy"
256 );
257 anyhow::Error::msg(e.to_string())
258 })?;
259
260 let token = self.get_token().await?;
261 let item_id = args["item_id"].as_str().ok_or_else(|| {
262 ::zeroclaw_log::record!(
263 WARN,
264 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
265 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
266 "mod: item_id is required"
267 );
268 anyhow::Error::msg("item_id is required")
269 })?;
270 let max_size = args["max_size"]
271 .as_u64()
272 .and_then(|v| usize::try_from(v).ok())
273 .unwrap_or(MAX_ONEDRIVE_DOWNLOAD_SIZE)
274 .min(MAX_ONEDRIVE_DOWNLOAD_SIZE);
275
276 let bytes = graph_client::onedrive_download(
277 &self.http_client,
278 &token,
279 self.user_id(),
280 item_id,
281 max_size,
282 )
283 .await?;
284
285 use base64::Engine;
287 let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
288
289 Ok(ToolResult {
290 success: true,
291 output: format!(
292 "Downloaded {} bytes (base64 encoded):\n{encoded}",
293 bytes.len()
294 ),
295 error: None,
296 })
297 }
298
299 async fn handle_sharepoint_search(
300 &self,
301 args: &serde_json::Value,
302 ) -> anyhow::Result<ToolResult> {
303 self.security
304 .enforce_tool_operation(ToolOperation::Read, "microsoft365.sharepoint_search")
305 .map_err(|e| {
306 ::zeroclaw_log::record!(
307 ERROR,
308 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
309 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
310 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
311 "microsoft365: tool operation denied by policy"
312 );
313 anyhow::Error::msg(e.to_string())
314 })?;
315
316 let token = self.get_token().await?;
317 let query = args["query"].as_str().ok_or_else(|| {
318 ::zeroclaw_log::record!(
319 WARN,
320 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
321 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
322 "mod: query is required"
323 );
324 anyhow::Error::msg("query is required")
325 })?;
326 let top = u32::try_from(args["top"].as_u64().unwrap_or(u64::from(DEFAULT_TOP)))
327 .unwrap_or(DEFAULT_TOP);
328
329 let result = graph_client::sharepoint_search(&self.http_client, &token, query, top).await?;
330
331 Ok(ToolResult {
332 success: true,
333 output: serde_json::to_string_pretty(&result)?,
334 error: None,
335 })
336 }
337
338 async fn handle_mail_send(&self, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
341 self.security
342 .enforce_tool_operation(ToolOperation::Act, "microsoft365.mail_send")
343 .map_err(|e| {
344 ::zeroclaw_log::record!(
345 ERROR,
346 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
347 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
348 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
349 "microsoft365: tool operation denied by policy"
350 );
351 anyhow::Error::msg(e.to_string())
352 })?;
353
354 let token = self.get_token().await?;
355 let to: Vec<String> = args["to"]
356 .as_array()
357 .ok_or_else(|| {
358 ::zeroclaw_log::record!(
359 WARN,
360 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
361 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
362 "mod: to must be an array of email addresses"
363 );
364 anyhow::Error::msg("to must be an array of email addresses")
365 })?
366 .iter()
367 .filter_map(|v| v.as_str().map(String::from))
368 .collect();
369
370 if to.is_empty() {
371 anyhow::bail!("to must contain at least one email address");
372 }
373
374 let subject = args["subject"].as_str().ok_or_else(|| {
375 ::zeroclaw_log::record!(
376 WARN,
377 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
378 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
379 "mod: subject is required"
380 );
381 anyhow::Error::msg("subject is required")
382 })?;
383 let body = args["body"].as_str().ok_or_else(|| {
384 ::zeroclaw_log::record!(
385 WARN,
386 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
387 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
388 "mod: body is required"
389 );
390 anyhow::Error::msg("body is required")
391 })?;
392
393 graph_client::mail_send(
394 &self.http_client,
395 &token,
396 self.user_id(),
397 &to,
398 subject,
399 body,
400 )
401 .await?;
402
403 Ok(ToolResult {
404 success: true,
405 output: format!("Email sent to: {}", to.join(", ")),
406 error: None,
407 })
408 }
409
410 async fn handle_teams_message_send(
411 &self,
412 args: &serde_json::Value,
413 ) -> anyhow::Result<ToolResult> {
414 self.security
415 .enforce_tool_operation(ToolOperation::Act, "microsoft365.teams_message_send")
416 .map_err(|e| {
417 ::zeroclaw_log::record!(
418 ERROR,
419 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
420 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
421 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
422 "microsoft365: tool operation denied by policy"
423 );
424 anyhow::Error::msg(e.to_string())
425 })?;
426
427 let token = self.get_token().await?;
428 let team_id = args["team_id"].as_str().ok_or_else(|| {
429 ::zeroclaw_log::record!(
430 WARN,
431 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
432 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
433 "mod: team_id is required"
434 );
435 anyhow::Error::msg("team_id is required")
436 })?;
437 let channel_id = args["channel_id"].as_str().ok_or_else(|| {
438 ::zeroclaw_log::record!(
439 WARN,
440 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
441 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
442 "mod: channel_id is required"
443 );
444 anyhow::Error::msg("channel_id is required")
445 })?;
446 let body = args["body"].as_str().ok_or_else(|| {
447 ::zeroclaw_log::record!(
448 WARN,
449 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
450 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
451 "mod: body is required"
452 );
453 anyhow::Error::msg("body is required")
454 })?;
455
456 graph_client::teams_message_send(&self.http_client, &token, team_id, channel_id, body)
457 .await?;
458
459 Ok(ToolResult {
460 success: true,
461 output: "Teams message sent".to_string(),
462 error: None,
463 })
464 }
465
466 async fn handle_calendar_event_create(
467 &self,
468 args: &serde_json::Value,
469 ) -> anyhow::Result<ToolResult> {
470 self.security
471 .enforce_tool_operation(ToolOperation::Act, "microsoft365.calendar_event_create")
472 .map_err(|e| {
473 ::zeroclaw_log::record!(
474 ERROR,
475 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
476 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
477 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
478 "microsoft365: tool operation denied by policy"
479 );
480 anyhow::Error::msg(e.to_string())
481 })?;
482
483 let token = self.get_token().await?;
484 let subject = args["subject"].as_str().ok_or_else(|| {
485 ::zeroclaw_log::record!(
486 WARN,
487 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
488 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
489 "mod: subject is required"
490 );
491 anyhow::Error::msg("subject is required")
492 })?;
493 let start = args["start"].as_str().ok_or_else(|| {
494 ::zeroclaw_log::record!(
495 WARN,
496 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
497 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
498 "mod: start datetime is required"
499 );
500 anyhow::Error::msg("start datetime is required")
501 })?;
502 let end = args["end"].as_str().ok_or_else(|| {
503 ::zeroclaw_log::record!(
504 WARN,
505 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
506 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
507 "mod: end datetime is required"
508 );
509 anyhow::Error::msg("end datetime is required")
510 })?;
511 let attendees: Vec<String> = args["attendees"]
512 .as_array()
513 .map(|arr| {
514 arr.iter()
515 .filter_map(|v| v.as_str().map(String::from))
516 .collect()
517 })
518 .unwrap_or_default();
519 let body_text = args["body"].as_str();
520
521 let event_id = graph_client::calendar_event_create(
522 &self.http_client,
523 &token,
524 self.user_id(),
525 subject,
526 start,
527 end,
528 &attendees,
529 body_text,
530 )
531 .await?;
532
533 Ok(ToolResult {
534 success: true,
535 output: format!("Calendar event created (id: {event_id})"),
536 error: None,
537 })
538 }
539
540 async fn handle_calendar_event_delete(
541 &self,
542 args: &serde_json::Value,
543 ) -> anyhow::Result<ToolResult> {
544 self.security
545 .enforce_tool_operation(ToolOperation::Act, "microsoft365.calendar_event_delete")
546 .map_err(|e| {
547 ::zeroclaw_log::record!(
548 ERROR,
549 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
550 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
551 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
552 "microsoft365: tool operation denied by policy"
553 );
554 anyhow::Error::msg(e.to_string())
555 })?;
556
557 let token = self.get_token().await?;
558 let event_id = args["event_id"].as_str().ok_or_else(|| {
559 ::zeroclaw_log::record!(
560 WARN,
561 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
562 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
563 "mod: event_id is required"
564 );
565 anyhow::Error::msg("event_id is required")
566 })?;
567
568 graph_client::calendar_event_delete(&self.http_client, &token, self.user_id(), event_id)
569 .await?;
570
571 Ok(ToolResult {
572 success: true,
573 output: format!("Calendar event {event_id} deleted"),
574 error: None,
575 })
576 }
577}
578
579#[async_trait]
580impl Tool for Microsoft365Tool {
581 fn name(&self) -> &str {
582 "microsoft365"
583 }
584
585 fn description(&self) -> &str {
586 "Microsoft 365 integration: manage Outlook mail, Teams messages, Calendar events, \
587 OneDrive files, and SharePoint search via Microsoft Graph API"
588 }
589
590 fn parameters_schema(&self) -> serde_json::Value {
591 json!({
592 "type": "object",
593 "required": ["action"],
594 "properties": {
595 "action": {
596 "type": "string",
597 "enum": [
598 "mail_list",
599 "mail_send",
600 "teams_message_list",
601 "teams_message_send",
602 "calendar_events_list",
603 "calendar_event_create",
604 "calendar_event_delete",
605 "onedrive_list",
606 "onedrive_download",
607 "sharepoint_search"
608 ],
609 "description": "The Microsoft 365 action to perform"
610 },
611 "folder": {
612 "type": "string",
613 "description": "Mail folder ID (for mail_list, e.g. 'inbox', 'sentitems')"
614 },
615 "to": {
616 "type": "array",
617 "items": { "type": "string" },
618 "description": "Recipient email addresses (for mail_send)"
619 },
620 "subject": {
621 "type": "string",
622 "description": "Email subject or calendar event subject"
623 },
624 "body": {
625 "type": "string",
626 "description": "Message body text"
627 },
628 "team_id": {
629 "type": "string",
630 "description": "Teams team ID (for teams_message_list/send)"
631 },
632 "channel_id": {
633 "type": "string",
634 "description": "Teams channel ID (for teams_message_list/send)"
635 },
636 "start": {
637 "type": "string",
638 "description": "Start datetime in ISO 8601 format (for calendar actions)"
639 },
640 "end": {
641 "type": "string",
642 "description": "End datetime in ISO 8601 format (for calendar actions)"
643 },
644 "attendees": {
645 "type": "array",
646 "items": { "type": "string" },
647 "description": "Attendee email addresses (for calendar_event_create)"
648 },
649 "event_id": {
650 "type": "string",
651 "description": "Calendar event ID (for calendar_event_delete)"
652 },
653 "path": {
654 "type": "string",
655 "description": "OneDrive folder path (for onedrive_list)"
656 },
657 "item_id": {
658 "type": "string",
659 "description": "OneDrive item ID (for onedrive_download)"
660 },
661 "max_size": {
662 "type": "integer",
663 "description": "Maximum download size in bytes (for onedrive_download, default 10MB)"
664 },
665 "query": {
666 "type": "string",
667 "description": "Search query (for sharepoint_search)"
668 },
669 "top": {
670 "type": "integer",
671 "description": "Maximum number of items to return (default 25)"
672 }
673 }
674 })
675 }
676
677 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
678 let action = match args["action"].as_str() {
679 Some(a) => a.to_string(),
680 None => {
681 return Ok(ToolResult {
682 success: false,
683 output: String::new(),
684 error: Some("'action' parameter is required".to_string()),
685 });
686 }
687 };
688
689 match self.dispatch(&action, &args).await {
690 Ok(result) => Ok(result),
691 Err(e) => Ok(ToolResult {
692 success: false,
693 output: String::new(),
694 error: Some(format!("microsoft365.{action} failed: {e}")),
695 }),
696 }
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703
704 #[test]
705 fn tool_name_is_microsoft365() {
706 let schema_str = r#"{"type":"object","required":["action"]}"#;
708 let _: serde_json::Value = serde_json::from_str(schema_str).unwrap();
709 }
710
711 #[test]
712 fn parameters_schema_has_action_enum() {
713 let schema = json!({
714 "type": "object",
715 "required": ["action"],
716 "properties": {
717 "action": {
718 "type": "string",
719 "enum": [
720 "mail_list",
721 "mail_send",
722 "teams_message_list",
723 "teams_message_send",
724 "calendar_events_list",
725 "calendar_event_create",
726 "calendar_event_delete",
727 "onedrive_list",
728 "onedrive_download",
729 "sharepoint_search"
730 ]
731 }
732 }
733 });
734
735 let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
736 assert_eq!(actions.len(), 10);
737 assert!(actions.contains(&json!("mail_list")));
738 assert!(actions.contains(&json!("sharepoint_search")));
739 }
740
741 #[test]
742 fn action_dispatch_table_is_exhaustive() {
743 let valid_actions = [
744 "mail_list",
745 "mail_send",
746 "teams_message_list",
747 "teams_message_send",
748 "calendar_events_list",
749 "calendar_event_create",
750 "calendar_event_delete",
751 "onedrive_list",
752 "onedrive_download",
753 "sharepoint_search",
754 ];
755 assert_eq!(valid_actions.len(), 10);
756 assert!(!valid_actions.contains(&"invalid_action"));
757 }
758}