1use serde::{Deserialize, Serialize};
2
3fn is_none_or_empty(s: &Option<String>) -> bool {
4 s.as_deref().is_none_or(str::is_empty)
5}
6
7pub trait Paginated: Sized {
12 fn next_page_token(&self) -> Option<&str>;
14 fn append_page(&mut self, next: Self);
16}
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
21pub struct Meeting {
22 pub id: u64,
23 pub topic: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub start_time: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub duration: Option<u32>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub join_url: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub start_url: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub status: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub created_at: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub password: Option<String>,
38 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
39 pub meeting_type: Option<u8>,
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct MeetingList {
44 pub meetings: Vec<Meeting>,
45 #[serde(skip_serializing_if = "is_none_or_empty")]
46 pub next_page_token: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub total_records: Option<u64>,
49 #[serde(skip_serializing)]
50 pub page_count: Option<u32>,
51 #[serde(skip_serializing)]
52 pub page_size: Option<u32>,
53}
54
55#[derive(Debug, Serialize)]
56pub struct CreateMeetingRequest {
57 pub topic: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub start_time: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub duration: Option<u32>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub password: Option<String>,
64 #[serde(rename = "type")]
65 pub meeting_type: u8,
66}
67
68#[derive(Debug, Serialize, Default)]
69pub struct UpdateMeetingRequest {
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub topic: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub start_time: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub duration: Option<u32>,
76}
77
78#[derive(Debug, Clone, Deserialize, Serialize)]
81pub struct User {
82 pub id: String,
83 pub email: String,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub display_name: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub first_name: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub last_name: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub status: Option<String>,
92 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
93 pub user_type: Option<u8>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub created_at: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub last_login_time: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub pic_url: Option<String>,
100}
101
102#[derive(Debug, Deserialize, Serialize)]
103pub struct UserList {
104 pub users: Vec<User>,
105 #[serde(skip_serializing_if = "is_none_or_empty")]
106 pub next_page_token: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub total_records: Option<u64>,
109 #[serde(skip_serializing)]
110 pub page_count: Option<u32>,
111 #[serde(skip_serializing)]
112 pub page_size: Option<u32>,
113}
114
115#[derive(Debug, Serialize)]
118pub struct CreateUserInfo {
119 pub email: String,
120 #[serde(rename = "type")]
121 pub user_type: u8,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub first_name: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub last_name: Option<String>,
126}
127
128#[derive(Debug, Serialize)]
129pub struct CreateUserRequest {
130 pub action: String,
131 pub user_info: CreateUserInfo,
132}
133
134#[derive(Debug, Serialize)]
135pub struct UserStatusRequest {
136 pub action: String,
137}
138
139#[derive(Debug, Clone, Deserialize, Serialize)]
142pub struct CloudRecording {
143 pub id: u64,
144 pub topic: String,
145 pub start_time: String,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub duration: Option<u32>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub total_size: Option<u64>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub recording_count: Option<u32>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub recording_files: Option<Vec<RecordingFile>>,
154}
155
156#[derive(Debug, Clone, Deserialize, Serialize)]
157pub struct RecordingFile {
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub id: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub file_type: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub file_extension: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub file_size: Option<u64>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub play_url: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub download_url: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub status: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub recording_type: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub recording_start: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub recording_end: Option<String>,
178}
179
180#[derive(Debug, Deserialize, Serialize)]
181pub struct RecordingList {
182 #[serde(rename(deserialize = "meetings", serialize = "recordings"))]
184 pub recordings: Option<Vec<CloudRecording>>,
185 #[serde(skip_serializing_if = "is_none_or_empty")]
186 pub next_page_token: Option<String>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub total_records: Option<u64>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub from: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub to: Option<String>,
193}
194
195#[derive(Debug, Serialize)]
198pub struct RecordingControlRequest {
199 pub action: String,
200}
201
202#[derive(Debug, Serialize)]
205pub struct MeetingStatusRequest {
206 pub action: String,
207}
208
209#[derive(Debug, Clone, Deserialize, Serialize)]
212pub struct MeetingInvitation {
213 pub invitation: String,
214}
215
216#[derive(Debug, Clone, Deserialize, Serialize)]
219pub struct Participant {
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub id: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub name: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub user_email: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub join_time: Option<String>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub leave_time: Option<String>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub duration: Option<u32>,
233}
234
235#[derive(Debug, Deserialize, Serialize)]
236pub struct ParticipantList {
237 pub participants: Vec<Participant>,
238 #[serde(skip_serializing_if = "is_none_or_empty")]
239 pub next_page_token: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub total_records: Option<u64>,
242 #[serde(skip_serializing)]
243 pub page_count: Option<u32>,
244 #[serde(skip_serializing)]
245 pub page_size: Option<u32>,
246}
247
248#[derive(Debug, Clone, Deserialize, Serialize)]
251pub struct MeetingReportItem {
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub uuid: Option<String>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub id: Option<u64>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub topic: Option<String>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub start_time: Option<String>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub end_time: Option<String>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub duration: Option<u32>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub total_minutes: Option<u32>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub participants_count: Option<u32>,
268}
269
270#[derive(Debug, Deserialize, Serialize)]
271pub struct UserMeetingReportList {
272 pub meetings: Vec<MeetingReportItem>,
273 #[serde(skip_serializing_if = "is_none_or_empty")]
274 pub next_page_token: Option<String>,
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub total_records: Option<u64>,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub from: Option<String>,
279 #[serde(skip_serializing_if = "Option::is_none")]
280 pub to: Option<String>,
281 #[serde(skip_serializing)]
282 pub page_count: Option<u32>,
283 #[serde(skip_serializing)]
284 pub page_size: Option<u32>,
285}
286
287#[derive(Debug, Clone, Deserialize, Serialize)]
290pub struct MeetingParticipantReport {
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub id: Option<String>,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub user_id: Option<String>,
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub name: Option<String>,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub user_email: Option<String>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub join_time: Option<String>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub leave_time: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub duration: Option<u64>,
306}
307
308#[derive(Debug, Deserialize, Serialize)]
309pub struct MeetingParticipantReportList {
310 pub participants: Vec<MeetingParticipantReport>,
311 #[serde(skip_serializing_if = "is_none_or_empty")]
312 pub next_page_token: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub total_records: Option<u64>,
315 #[serde(skip_serializing)]
316 pub page_size: Option<u32>,
317}
318
319#[derive(Debug, Clone, Deserialize, Serialize)]
322pub struct Webinar {
323 pub id: u64,
324 pub topic: String,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub start_time: Option<String>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub duration: Option<u32>,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub join_url: Option<String>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub agenda: Option<String>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub status: Option<String>,
335 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
336 pub webinar_type: Option<u8>,
337}
338
339#[derive(Debug, Deserialize, Serialize)]
340pub struct WebinarList {
341 pub webinars: Vec<Webinar>,
342 #[serde(skip_serializing_if = "is_none_or_empty")]
343 pub next_page_token: Option<String>,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub total_records: Option<u64>,
346 #[serde(skip_serializing)]
347 pub page_size: Option<u32>,
348}
349
350impl Paginated for MeetingList {
353 fn next_page_token(&self) -> Option<&str> {
354 self.next_page_token.as_deref().filter(|s| !s.is_empty())
355 }
356
357 fn append_page(&mut self, next: Self) {
358 self.meetings.extend(next.meetings);
359 self.next_page_token = next.next_page_token.filter(|t| !t.is_empty());
361 }
362}
363
364impl Paginated for UserList {
365 fn next_page_token(&self) -> Option<&str> {
366 self.next_page_token.as_deref().filter(|s| !s.is_empty())
367 }
368
369 fn append_page(&mut self, next: Self) {
370 self.users.extend(next.users);
371 self.next_page_token = next.next_page_token.filter(|t| !t.is_empty());
372 }
373}
374
375impl Paginated for RecordingList {
376 fn next_page_token(&self) -> Option<&str> {
377 self.next_page_token.as_deref().filter(|s| !s.is_empty())
378 }
379
380 fn append_page(&mut self, next: Self) {
381 match (self.recordings.as_mut(), next.recordings) {
382 (Some(dst), Some(src)) => dst.extend(src),
383 (None, Some(src)) => self.recordings = Some(src),
384 _ => {}
385 }
386 self.next_page_token = next.next_page_token.filter(|t| !t.is_empty());
387 }
388}
389
390impl Paginated for ParticipantList {
391 fn next_page_token(&self) -> Option<&str> {
392 self.next_page_token.as_deref().filter(|s| !s.is_empty())
393 }
394
395 fn append_page(&mut self, next: Self) {
396 self.participants.extend(next.participants);
397 self.next_page_token = next.next_page_token.filter(|t| !t.is_empty());
398 }
399}
400
401impl Paginated for UserMeetingReportList {
402 fn next_page_token(&self) -> Option<&str> {
403 self.next_page_token.as_deref().filter(|s| !s.is_empty())
404 }
405
406 fn append_page(&mut self, next: Self) {
407 self.meetings.extend(next.meetings);
408 self.next_page_token = next.next_page_token.filter(|t| !t.is_empty());
409 }
410}
411
412impl Paginated for MeetingParticipantReportList {
413 fn next_page_token(&self) -> Option<&str> {
414 self.next_page_token.as_deref().filter(|s| !s.is_empty())
415 }
416 fn append_page(&mut self, next: Self) {
417 self.participants.extend(next.participants);
418 self.next_page_token = next.next_page_token.filter(|t| !t.is_empty());
419 }
420}
421
422impl Paginated for WebinarList {
423 fn next_page_token(&self) -> Option<&str> {
424 self.next_page_token.as_deref().filter(|s| !s.is_empty())
425 }
426
427 fn append_page(&mut self, next: Self) {
428 self.webinars.extend(next.webinars);
429 self.next_page_token = next.next_page_token.filter(|t| !t.is_empty());
430 }
431}
432
433#[derive(Debug, Deserialize)]
436pub struct TokenResponse {
437 pub access_token: String,
438 pub token_type: String,
439 pub expires_in: u64,
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn meeting_deserializes_from_zoom_api_shape() {
448 let json = r#"{
449 "id": 123456789,
450 "topic": "Weekly Standup",
451 "start_time": "2026-04-03T09:00:00Z",
452 "duration": 30,
453 "join_url": "https://zoom.us/j/123456789",
454 "start_url": "https://zoom.us/s/123456789",
455 "status": "waiting",
456 "created_at": "2026-04-02T10:00:00Z",
457 "type": 2
458 }"#;
459 let m: Meeting = serde_json::from_str(json).unwrap();
460 assert_eq!(m.id, 123456789);
461 assert_eq!(m.topic, "Weekly Standup");
462 assert_eq!(m.duration, Some(30));
463 assert_eq!(m.meeting_type, Some(2));
464 }
465
466 #[test]
467 fn meeting_serializes_skipping_null_fields() {
468 let m = Meeting {
469 id: 1,
470 topic: "Test".into(),
471 start_time: None,
472 duration: Some(30),
473 join_url: None,
474 start_url: None,
475 status: None,
476 created_at: None,
477 password: None,
478 meeting_type: Some(2),
479 };
480 let v: serde_json::Value = serde_json::to_value(&m).unwrap();
481 assert!(v.get("start_time").is_none(), "null fields must be omitted");
482 assert!(v.get("join_url").is_none(), "null fields must be omitted");
483 assert_eq!(v["duration"], 30);
484 }
485
486 #[test]
487 fn meeting_list_deserializes_with_pagination() {
488 let json = r#"{
489 "meetings": [
490 {"id": 111, "topic": "A"},
491 {"id": 222, "topic": "B"}
492 ],
493 "total_records": 2,
494 "page_count": 1,
495 "page_size": 30
496 }"#;
497 let list: MeetingList = serde_json::from_str(json).unwrap();
498 assert_eq!(list.meetings.len(), 2);
499 assert_eq!(list.total_records, Some(2));
500 assert!(list.next_page_token.is_none());
501 }
502
503 #[test]
504 fn meeting_list_serializes_without_page_internals() {
505 let list = MeetingList {
506 meetings: vec![],
507 next_page_token: Some("".into()),
508 total_records: Some(0),
509 page_count: Some(1),
510 page_size: Some(100),
511 };
512 let v: serde_json::Value = serde_json::to_value(&list).unwrap();
513 assert!(
514 v.get("page_count").is_none(),
515 "page internals must be hidden"
516 );
517 assert!(
518 v.get("page_size").is_none(),
519 "page internals must be hidden"
520 );
521 assert!(
522 v.get("next_page_token").is_none(),
523 "empty token must be omitted"
524 );
525 assert_eq!(v["total_records"], 0);
526 }
527
528 #[test]
529 fn user_deserializes_from_zoom_api_shape() {
530 let json = r#"{
531 "id": "user-abc-123",
532 "email": "alice@example.com",
533 "display_name": "Alice Smith",
534 "first_name": "Alice",
535 "last_name": "Smith",
536 "status": "active",
537 "type": 2
538 }"#;
539 let u: User = serde_json::from_str(json).unwrap();
540 assert_eq!(u.id, "user-abc-123");
541 assert_eq!(u.email, "alice@example.com");
542 assert_eq!(u.display_name, Some("Alice Smith".into()));
543 assert_eq!(u.user_type, Some(2));
544 }
545
546 #[test]
547 fn user_serializes_skipping_null_fields() {
548 let u = User {
549 id: "u1".into(),
550 email: "a@b.com".into(),
551 display_name: Some("Alice".into()),
552 first_name: None,
553 last_name: None,
554 status: Some("active".into()),
555 user_type: Some(2),
556 created_at: None,
557 last_login_time: None,
558 pic_url: None,
559 };
560 let v: serde_json::Value = serde_json::to_value(&u).unwrap();
561 assert!(v.get("first_name").is_none());
562 assert!(v.get("pic_url").is_none());
563 assert_eq!(v["display_name"], "Alice");
564 }
565
566 #[test]
567 fn recording_file_deserializes_with_optional_fields() {
568 let json = r#"{
569 "id": "rec-file-001",
570 "file_type": "MP4",
571 "file_size": 104857600,
572 "download_url": "https://zoom.us/rec/download/abc123",
573 "status": "completed",
574 "recording_type": "shared_screen_with_speaker_view"
575 }"#;
576 let f: RecordingFile = serde_json::from_str(json).unwrap();
577 assert_eq!(f.file_type, Some("MP4".into()));
578 assert_eq!(f.file_size, Some(104857600));
579 }
580
581 #[test]
582 fn recording_list_deserializes_meetings_key() {
583 let json = r#"{
584 "meetings": [
585 {"id": 1, "topic": "Test", "start_time": "2026-01-01T10:00:00Z"}
586 ],
587 "total_records": 1,
588 "from": "2026-01-01",
589 "to": "2026-04-01",
590 "next_page_token": ""
591 }"#;
592 let list: RecordingList = serde_json::from_str(json).unwrap();
593 assert_eq!(list.recordings.as_ref().unwrap().len(), 1);
594 assert_eq!(list.total_records, Some(1));
595 }
596
597 #[test]
598 fn recording_list_serializes_recordings_key() {
599 let list = RecordingList {
600 recordings: Some(vec![]),
601 next_page_token: Some("".into()),
602 total_records: Some(0),
603 from: Some("2026-01-01".into()),
604 to: Some("2026-04-01".into()),
605 };
606 let v: serde_json::Value = serde_json::to_value(&list).unwrap();
607 assert!(
608 v.get("meetings").is_none(),
609 "must serialize as 'recordings', not 'meetings'"
610 );
611 assert!(v.get("recordings").is_some());
612 assert!(
613 v.get("next_page_token").is_none(),
614 "empty token must be omitted"
615 );
616 }
617
618 #[test]
619 fn token_response_deserializes() {
620 let json = r#"{
621 "access_token": "eyJhbGc...",
622 "token_type": "bearer",
623 "expires_in": 3599
624 }"#;
625 let t: TokenResponse = serde_json::from_str(json).unwrap();
626 assert_eq!(t.access_token, "eyJhbGc...");
627 assert_eq!(t.expires_in, 3599);
628 }
629
630 #[test]
631 fn create_meeting_request_serializes_skipping_nones() {
632 let req = CreateMeetingRequest {
633 topic: "Demo".into(),
634 start_time: None,
635 duration: Some(45),
636 password: None,
637 meeting_type: 2,
638 };
639 let json: serde_json::Value = serde_json::to_value(&req).unwrap();
640 assert_eq!(json["topic"], "Demo");
641 assert_eq!(json["duration"], 45);
642 assert_eq!(json["type"], 2);
643 assert!(
644 json.get("start_time").is_none(),
645 "None fields must be omitted"
646 );
647 assert!(
648 json.get("password").is_none(),
649 "None fields must be omitted"
650 );
651 }
652
653 #[test]
654 fn participant_list_deserializes() {
655 let json = r#"{
656 "participants": [
657 {
658 "id": "u1",
659 "name": "Alice",
660 "user_email": "alice@example.com",
661 "join_time": "2026-04-01T10:00:00Z",
662 "leave_time": "2026-04-01T11:00:00Z",
663 "duration": 3600
664 }
665 ],
666 "total_records": 1,
667 "page_size": 300,
668 "next_page_token": ""
669 }"#;
670 let list: ParticipantList = serde_json::from_str(json).unwrap();
671 assert_eq!(list.participants.len(), 1);
672 assert_eq!(list.participants[0].name, Some("Alice".into()));
673 assert_eq!(list.participants[0].duration, Some(3600));
674 let v: serde_json::Value = serde_json::to_value(&list).unwrap();
676 assert!(v.get("page_size").is_none());
677 assert!(v.get("next_page_token").is_none());
678 }
679
680 #[test]
681 fn meeting_list_paginated_append_merges_items() {
682 let mut first = MeetingList {
683 meetings: vec![Meeting {
684 id: 1,
685 topic: "First".into(),
686 start_time: None,
687 duration: None,
688 join_url: None,
689 start_url: None,
690 status: None,
691 created_at: None,
692 password: None,
693 meeting_type: None,
694 }],
695 next_page_token: Some("tok".into()),
696 total_records: Some(2),
697 page_count: None,
698 page_size: None,
699 };
700 let second = MeetingList {
701 meetings: vec![Meeting {
702 id: 2,
703 topic: "Second".into(),
704 start_time: None,
705 duration: None,
706 join_url: None,
707 start_url: None,
708 status: None,
709 created_at: None,
710 password: None,
711 meeting_type: None,
712 }],
713 next_page_token: Some("".into()),
714 total_records: Some(2),
715 page_count: None,
716 page_size: None,
717 };
718 first.append_page(second);
719 assert_eq!(first.meetings.len(), 2);
720 assert_eq!(first.next_page_token(), None, "empty token is exhausted");
721 assert_eq!(
722 first.total_records,
723 Some(2),
724 "total_records preserved from first page"
725 );
726 }
727
728 #[test]
729 fn recording_list_paginated_append_merges_option_vec() {
730 let mut first = RecordingList {
731 recordings: Some(vec![CloudRecording {
732 id: 1,
733 topic: "Rec 1".into(),
734 start_time: "2026-01-01T10:00:00Z".into(),
735 duration: None,
736 total_size: None,
737 recording_count: None,
738 recording_files: None,
739 }]),
740 next_page_token: Some("t".into()),
741 total_records: Some(2),
742 from: None,
743 to: None,
744 };
745 let second = RecordingList {
746 recordings: Some(vec![CloudRecording {
747 id: 2,
748 topic: "Rec 2".into(),
749 start_time: "2026-01-02T10:00:00Z".into(),
750 duration: None,
751 total_size: None,
752 recording_count: None,
753 recording_files: None,
754 }]),
755 next_page_token: None,
756 total_records: Some(2),
757 from: None,
758 to: None,
759 };
760 first.append_page(second);
761 assert_eq!(first.recordings.as_ref().unwrap().len(), 2);
762 assert_eq!(first.next_page_token(), None);
763 }
764
765 #[test]
766 fn webinar_list_deserializes_and_serializes() {
767 let json = r#"{
768 "webinars": [
769 {
770 "id": 12345678,
771 "topic": "Product Launch",
772 "start_time": "2026-05-01T14:00:00Z",
773 "duration": 60,
774 "type": 5
775 }
776 ],
777 "total_records": 1,
778 "page_size": 300
779 }"#;
780 let list: WebinarList = serde_json::from_str(json).unwrap();
781 assert_eq!(list.webinars.len(), 1);
782 assert_eq!(list.webinars[0].id, 12345678);
783 assert_eq!(list.webinars[0].topic, "Product Launch");
784 assert_eq!(list.webinars[0].webinar_type, Some(5));
785 let v: serde_json::Value = serde_json::to_value(&list).unwrap();
787 assert!(v.get("page_size").is_none());
788 }
789
790 #[test]
791 fn webinar_list_paginated_append_merges_items() {
792 let mut first = WebinarList {
793 webinars: vec![Webinar {
794 id: 1,
795 topic: "First".into(),
796 start_time: None,
797 duration: None,
798 join_url: None,
799 agenda: None,
800 status: None,
801 webinar_type: None,
802 }],
803 next_page_token: Some("tok".into()),
804 total_records: Some(2),
805 page_size: None,
806 };
807 let second = WebinarList {
808 webinars: vec![Webinar {
809 id: 2,
810 topic: "Second".into(),
811 start_time: None,
812 duration: None,
813 join_url: None,
814 agenda: None,
815 status: None,
816 webinar_type: None,
817 }],
818 next_page_token: Some("".into()),
819 total_records: Some(2),
820 page_size: None,
821 };
822 first.append_page(second);
823 assert_eq!(first.webinars.len(), 2);
824 assert_eq!(first.next_page_token(), None);
825 }
826
827 #[test]
828 fn meeting_report_list_deserializes() {
829 let json = r#"{
830 "meetings": [
831 {
832 "uuid": "abc==",
833 "id": 123,
834 "topic": "Standup",
835 "start_time": "2026-04-01T09:00:00Z",
836 "duration": 30,
837 "participants_count": 5
838 }
839 ],
840 "total_records": 1,
841 "from": "2026-04-01",
842 "to": "2026-04-30"
843 }"#;
844 let list: UserMeetingReportList = serde_json::from_str(json).unwrap();
845 assert_eq!(list.meetings.len(), 1);
846 assert_eq!(list.meetings[0].participants_count, Some(5));
847 }
848}