1use std::sync::Arc;
2
3use reqwest::Method;
4use types::{ListRecipientsOptions, UpdateBroadcastOptions, UpdateBroadcastResponse};
5
6use crate::{Config, Result, list_opts::ListResponse};
7use crate::{
8 list_opts::ListOptions,
9 types::{
10 Broadcast, BroadcastClickedLink, BroadcastRecipient, CancelBroadcastResponse,
11 CreateBroadcastOptions, CreateBroadcastResponse, RemoveBroadcastResponse,
12 SendBroadcastOptions, SendBroadcastResponse,
13 },
14};
15
16#[derive(Clone, Debug)]
18pub struct BroadcastsSvc(pub(crate) Arc<Config>);
19
20impl BroadcastsSvc {
21 #[maybe_async::maybe_async]
25 pub async fn create(
26 &self,
27 broadcast: CreateBroadcastOptions,
28 ) -> Result<CreateBroadcastResponse> {
29 let request = self.0.build(Method::POST, "/broadcasts");
30 let response = self.0.send(request.json(&broadcast)).await?;
31 let content = response.json::<CreateBroadcastResponse>().await?;
32
33 Ok(content)
34 }
35
36 #[maybe_async::maybe_async]
40 pub async fn send(&self, broadcast: SendBroadcastOptions) -> Result<SendBroadcastResponse> {
41 let path = format!("/broadcasts/{}/send", broadcast.broadcast_id);
42
43 let request = self.0.build(Method::POST, &path);
44 let response = self.0.send(request.json(&broadcast)).await?;
45 let content = response.json::<SendBroadcastResponse>().await?;
46
47 Ok(content)
48 }
49
50 #[maybe_async::maybe_async]
56 pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<Broadcast>> {
57 let request = self.0.build(Method::GET, "/broadcasts").query(&list_opts);
58 let response = self.0.send(request).await?;
59 let content = response.json::<ListResponse<Broadcast>>().await?;
60
61 Ok(content)
62 }
63
64 #[maybe_async::maybe_async]
68 pub async fn get(&self, broadcast_id: &str) -> Result<Broadcast> {
69 let path = format!("/broadcasts/{broadcast_id}");
70
71 let request = self.0.build(Method::GET, &path);
72 let response = self.0.send(request).await?;
73 let content = response.json::<Broadcast>().await?;
74
75 Ok(content)
76 }
77
78 #[maybe_async::maybe_async]
79 pub async fn cancel(&self, broadcast_id: &str) -> Result<CancelBroadcastResponse> {
80 let path = format!("/broadcasts/{broadcast_id}/cancel");
81
82 let request = self.0.build(Method::POST, &path);
83 let response = self.0.send(request).await?;
84 let content = response.json::<CancelBroadcastResponse>().await?;
85
86 Ok(content)
87 }
88
89 #[maybe_async::maybe_async]
93 pub async fn delete(&self, broadcast_id: &str) -> Result<bool> {
94 let path = format!("/broadcasts/{broadcast_id}");
95
96 let request = self.0.build(Method::DELETE, &path);
97 let response = self.0.send(request).await?;
98 let content = response.json::<RemoveBroadcastResponse>().await?;
99
100 Ok(content.deleted)
101 }
102
103 #[maybe_async::maybe_async]
107 pub async fn clicked_links<T>(
108 &self,
109 broadcast_id: &str,
110 list_opts: ListOptions<T>,
111 ) -> Result<ListResponse<BroadcastClickedLink>> {
112 let path = format!("/broadcasts/{broadcast_id}/clicked-links");
113
114 let request = self.0.build(Method::GET, &path).query(&list_opts);
115 let response = self.0.send(request).await?;
116 let content = response
117 .json::<ListResponse<BroadcastClickedLink>>()
118 .await?;
119
120 Ok(content)
121 }
122
123 #[maybe_async::maybe_async]
125 pub async fn update(
126 &self,
127 broadcast_id: &str,
128 update: UpdateBroadcastOptions,
129 ) -> Result<UpdateBroadcastResponse> {
130 let path = format!("/broadcasts/{broadcast_id}");
131
132 let request = self.0.build(Method::PATCH, &path);
133 let response = self.0.send(request.json(&update)).await?;
134 let content = response.json::<UpdateBroadcastResponse>().await?;
135
136 Ok(content)
137 }
138
139 #[maybe_async::maybe_async]
146 pub async fn recipients<T>(
147 &self,
148 broadcast_id: &str,
149 list_opts: ListRecipientsOptions<T>,
150 ) -> Result<ListResponse<BroadcastRecipient>> {
151 let path = format!("/broadcasts/{broadcast_id}/recipients");
152
153 let request = self.0.build(Method::GET, &path).query(&list_opts);
154 let response = self.0.send(request).await?;
155 let content = response.json::<ListResponse<BroadcastRecipient>>().await?;
156
157 Ok(content)
158 }
159}
160
161#[allow(unreachable_pub)]
162pub mod types {
163 use ecow::EcoString;
164 use serde::{Deserialize, Serialize};
165
166 use crate::{
167 list_opts::{ListAfter, ListBefore, ListOptions, TimeNotSpecified},
168 types::{ContactId, SegmentId},
169 };
170
171 #[must_use]
173 #[derive(Debug, Clone, Serialize)]
174 pub struct CreateBroadcastOptions {
175 audience_id: String,
176 from: String,
177 subject: String,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 reply_to: Option<Vec<String>>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 html: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 text: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 name: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 send: Option<bool>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 scheduled_at: Option<String>,
190 }
191
192 impl CreateBroadcastOptions {
193 pub fn new(audience_id: &str, from: &str, subject: &str) -> Self {
199 Self {
200 audience_id: audience_id.to_string(),
201 from: from.to_string(),
202 subject: subject.to_string(),
203 reply_to: None,
204 html: None,
205 text: None,
206 name: None,
207 send: None,
208 scheduled_at: None,
209 }
210 }
211
212 #[inline]
214 pub fn with_reply(mut self, to: &str) -> Self {
215 let reply_to = self.reply_to.get_or_insert_with(Vec::new);
216 reply_to.push(to.to_owned());
217 self
218 }
219
220 #[inline]
222 pub fn with_reply_multiple(mut self, to: &[String]) -> Self {
223 let reply_to = self.reply_to.get_or_insert_with(Vec::new);
224 reply_to.extend_from_slice(to);
225 self
226 }
227
228 #[inline]
230 pub fn with_html(mut self, html: &str) -> Self {
231 self.html = Some(html.to_owned());
232 self
233 }
234
235 #[inline]
237 pub fn with_text(mut self, text: &str) -> Self {
238 self.text = Some(text.to_owned());
239 self
240 }
241
242 #[inline]
244 pub fn with_name(mut self, name: &str) -> Self {
245 self.name = Some(name.to_owned());
246 self
247 }
248
249 #[inline]
253 pub fn with_send(mut self, send: bool) -> Self {
254 self.send = Some(send);
255 self
256 }
257
258 #[inline]
261 pub fn with_scheduled_at(mut self, scheduled_at: &str) -> Self {
262 self.scheduled_at = Some(scheduled_at.to_owned());
263 self
264 }
265 }
266
267 #[must_use]
268 #[derive(Debug, Clone, Serialize, Default)]
269 pub struct UpdateBroadcastOptions {
270 #[serde(skip_serializing_if = "Option::is_none")]
271 from: Option<String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 subject: Option<String>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 reply_to: Option<Vec<String>>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 html: Option<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 text: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 name: Option<String>,
282 }
283
284 impl UpdateBroadcastOptions {
285 pub fn new() -> Self {
287 Self::default()
288 }
289
290 #[inline]
292 pub fn with_from(mut self, from: &str) -> Self {
293 self.from = Some(from.to_owned());
294 self
295 }
296
297 #[inline]
299 pub fn with_subject(mut self, subject: &str) -> Self {
300 self.subject = Some(subject.to_owned());
301 self
302 }
303
304 pub fn with_reply(mut self, to: &str) -> Self {
306 let reply_to = self.reply_to.get_or_insert_with(Vec::new);
307 reply_to.push(to.to_owned());
308 self
309 }
310
311 #[inline]
313 pub fn with_reply_multiple(mut self, to: &[String]) -> Self {
314 let reply_to = self.reply_to.get_or_insert_with(Vec::new);
315 reply_to.extend_from_slice(to);
316 self
317 }
318
319 #[inline]
321 pub fn with_html(mut self, html: &str) -> Self {
322 self.html = Some(html.to_owned());
323 self
324 }
325
326 #[inline]
328 pub fn with_text(mut self, text: &str) -> Self {
329 self.text = Some(text.to_owned());
330 self
331 }
332
333 #[inline]
335 pub fn with_name(mut self, name: &str) -> Self {
336 self.name = Some(name.to_owned());
337 self
338 }
339 }
340
341 #[derive(Debug, Clone, Serialize, Deserialize)]
342 pub struct UpdateBroadcastResponse {
343 pub id: BroadcastId,
345 }
346
347 crate::define_id_type!(BroadcastId);
348
349 #[derive(Debug, Clone, Serialize, Deserialize)]
350 pub struct CreateBroadcastResponse {
351 pub id: BroadcastId,
353 }
354
355 #[must_use]
356 #[derive(Debug, Clone, Serialize)]
357 pub struct SendBroadcastOptions {
358 pub(crate) broadcast_id: BroadcastId,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
361 scheduled_at: Option<String>,
362 }
363
364 impl SendBroadcastOptions {
365 pub fn new(broadcast_id: &str) -> Self {
366 let broadcast_id = BroadcastId(EcoString::from(broadcast_id.to_owned()));
367
368 Self {
369 broadcast_id,
370 scheduled_at: None,
371 }
372 }
373
374 #[inline]
377 pub fn with_scheduled_at(mut self, scheduled_at: &str) -> Self {
378 self.scheduled_at = Some(scheduled_at.to_owned());
379 self
380 }
381 }
382
383 #[derive(Debug, Clone, Serialize, Deserialize)]
384 pub struct SendBroadcastResponse {
385 pub id: BroadcastId,
387 }
388
389 #[must_use]
390 #[derive(Debug, Clone, Serialize, Deserialize)]
391 pub struct Broadcast {
392 pub id: BroadcastId,
393 pub name: String,
394 pub audience_id: SegmentId,
395 pub status: String,
396 pub created_at: String,
397 pub scheduled_at: Option<String>,
398 pub sent_at: Option<String>,
399 pub from: Option<String>,
400 pub subject: Option<String>,
401 pub reply_to: Option<Vec<String>>,
402 pub preview_text: Option<String>,
403 pub text: Option<String>,
404 pub html: Option<String>,
405 }
406
407 #[derive(Debug, Clone, Serialize, Deserialize)]
408 pub struct CancelBroadcastResponse {
409 pub id: BroadcastId,
410 }
411
412 #[derive(Debug, Clone, Serialize, Deserialize)]
413 pub struct RemoveBroadcastResponse {
414 #[allow(dead_code)]
416 pub id: BroadcastId,
417 pub deleted: bool,
419 }
420
421 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
423 #[must_use]
424 #[serde(rename_all = "snake_case")]
425 pub enum BroadcastRecipientEventType {
426 Sent,
427 Delivered,
428 Opened,
429 Clicked,
430 Bounced,
431 Complained,
432 Unsubscribed,
433 Suppressed,
434 }
435
436 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
438 #[must_use]
439 #[serde(rename_all = "snake_case")]
440 pub enum BroadcastRecipientBounceType {
441 Permanent,
442 Transient,
443 Undetermined,
444 }
445
446 #[must_use]
461 #[derive(Debug, Clone, Serialize)]
462 #[serde(bound(serialize = ""))]
466 pub struct ListRecipientsOptions<List = TimeNotSpecified> {
467 #[serde(rename = "type")]
468 r#type: BroadcastRecipientEventType,
469
470 #[serde(skip_serializing_if = "Option::is_none")]
471 email: Option<String>,
472
473 #[serde(skip_serializing_if = "Option::is_none")]
474 bounce_type: Option<BroadcastRecipientBounceType>,
475
476 #[serde(flatten)]
477 pagination: ListOptions<List>,
478 }
479
480 impl ListRecipientsOptions<TimeNotSpecified> {
481 pub fn new(event_type: BroadcastRecipientEventType) -> Self {
484 Self {
485 r#type: event_type,
486 email: None,
487 bounce_type: None,
488 pagination: ListOptions::default(),
489 }
490 }
491
492 #[inline]
495 pub fn list_before(self, id: &str) -> ListRecipientsOptions<ListBefore> {
496 ListRecipientsOptions {
497 r#type: self.r#type,
498 email: self.email,
499 bounce_type: self.bounce_type,
500 pagination: self.pagination.list_before(id),
501 }
502 }
503
504 #[inline]
507 pub fn list_after(self, id: &str) -> ListRecipientsOptions<ListAfter> {
508 ListRecipientsOptions {
509 r#type: self.r#type,
510 email: self.email,
511 bounce_type: self.bounce_type,
512 pagination: self.pagination.list_after(id),
513 }
514 }
515 }
516
517 impl<T> ListRecipientsOptions<T> {
518 #[inline]
524 pub fn with_limit(mut self, limit: u8) -> Self {
525 self.pagination = self.pagination.with_limit(limit);
526 self
527 }
528
529 #[inline]
531 pub fn with_email(mut self, email: &str) -> Self {
532 self.email = Some(email.to_owned());
533 self
534 }
535
536 #[inline]
540 pub fn with_bounce_type(mut self, bounce_type: BroadcastRecipientBounceType) -> Self {
541 self.bounce_type = Some(bounce_type);
542 self
543 }
544 }
545
546 #[must_use]
548 #[derive(Debug, Clone, Serialize, Deserialize)]
549 pub struct BroadcastRecipientClickedLink {
550 pub url: String,
551 pub clicks: u32,
552 }
553
554 #[must_use]
558 #[derive(Debug, Clone, Serialize, Deserialize)]
559 pub struct BroadcastRecipient {
560 pub id: String,
563 pub contact_id: Option<ContactId>,
566 pub email: String,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub count: Option<u32>,
571 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub bounce_type: Option<BroadcastRecipientBounceType>,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
576 pub clicked_links: Option<Vec<BroadcastRecipientClickedLink>>,
577 }
578
579 #[derive(Debug, Clone, Serialize, Deserialize)]
580 pub struct BroadcastClickedLink {
581 pub id: String,
584 pub url: String,
586 pub clicks: u64,
588 pub unique_clicks: u64,
590 }
591}
592
593#[cfg(test)]
594#[allow(clippy::needless_return, clippy::indexing_slicing)]
595mod test {
596 #[cfg(not(feature = "blocking"))]
597 use crate::{
598 list_opts::ListOptions,
599 test::{CLIENT, DebugResult},
600 types::{
601 CreateBroadcastOptions, CreateContactOptions, SendBroadcastOptions,
602 UpdateBroadcastOptions,
603 },
604 };
605
606 use super::types::{
607 Broadcast, BroadcastClickedLink, BroadcastRecipient, BroadcastRecipientEventType,
608 CancelBroadcastResponse, ListRecipientsOptions,
609 };
610
611 #[tokio_shared_rt::test(shared = true)]
612 #[serial_test::serial]
613 #[cfg(not(feature = "blocking"))]
614 #[ignore = "Can no longer send broadcasts from the resend.dev domain"]
615 async fn create_send_broadcast() -> DebugResult<()> {
616 let resend = &*CLIENT;
617 std::thread::sleep(std::time::Duration::from_secs(1));
618
619 let audience_id = resend.segments.create("audience").await?.id;
620
621 let contact = CreateContactOptions::new("steve.wozniak@gmail.com")
622 .with_first_name("Steve")
623 .with_last_name("Wozniak")
624 .with_unsubscribed(false)
625 .with_audience_id(&audience_id);
626
627 let _contact_id = resend.contacts.create(contact).await?;
628
629 let from = "Acme <onboarding@resend.dev>";
630 let subject = "hello world";
631 let html =
632 "Hi {{{FIRST_NAME|there}}}, you can unsubscribe here: {{{RESEND_UNSUBSCRIBE_URL}}}";
633
634 std::thread::sleep(std::time::Duration::from_secs(2));
635
636 let broadcast = CreateBroadcastOptions::new(&audience_id, from, subject).with_html(html);
638 let res = resend.broadcasts.create(broadcast).await?;
639
640 std::thread::sleep(std::time::Duration::from_secs(4));
641
642 let opts = SendBroadcastOptions::new(&res.id);
644 let _res = resend.broadcasts.send(opts).await?;
645
646 std::thread::sleep(std::time::Duration::from_secs(2));
648
649 let deleted = resend.segments.delete(&audience_id).await?;
650 std::thread::sleep(std::time::Duration::from_secs(1));
651
652 assert!(deleted);
653
654 Ok(())
655 }
656
657 #[tokio_shared_rt::test(shared = true)]
658 #[serial_test::serial]
659 #[cfg(not(feature = "blocking"))]
660 #[ignore = "Can no longer send broadcasts from the resend.dev domain"]
661 async fn list_get_broadcast() -> DebugResult<()> {
662 let resend = &*CLIENT;
663 std::thread::sleep(std::time::Duration::from_secs(1));
664
665 let broadcasts = resend.broadcasts.list(ListOptions::default()).await?;
666 assert!(!broadcasts.data.is_empty(), "No broadcasts found");
667 let broadcast = broadcasts[0].clone();
668
669 let _res = resend.broadcasts.get(&broadcast.id.clone()).await?;
670 let _deleted = resend.broadcasts.delete(&broadcast.id).await;
671 let audience_id = resend.segments.create("audience").await?.id;
677 let from = "Acme <onboarding@resend.dev>";
678 let subject = "hello world";
679 let text = "text";
680
681 let broadcast = CreateBroadcastOptions::new(&audience_id, from, subject).with_text(text);
682 let res = resend.broadcasts.create(broadcast).await?;
683 std::thread::sleep(std::time::Duration::from_secs(2));
684 let deleted_broadcast = resend.broadcasts.delete(&res.id).await;
685 let deleted_audience = resend.segments.delete(&audience_id).await;
686 std::thread::sleep(std::time::Duration::from_secs(1));
687
688 assert!(deleted_broadcast.is_ok());
689 assert!(deleted_audience.is_ok());
690
691 Ok(())
692 }
693
694 #[tokio_shared_rt::test(shared = true)]
695 #[serial_test::serial]
696 #[cfg(not(feature = "blocking"))]
697 #[ignore = "Can no longer send broadcasts from the resend.dev domain"]
698 async fn update_broadcast() -> DebugResult<()> {
699 let resend = &*CLIENT;
700 std::thread::sleep(std::time::Duration::from_secs(1));
701
702 let audience_id = resend.segments.create("audience").await?.id;
704 let from = "Acme <onboarding@resend.dev>";
705 let subject = "hello world";
706
707 let create_broadcast =
708 CreateBroadcastOptions::new(&audience_id, from, subject).with_text("text");
709 let broadcast_id = resend.broadcasts.create(create_broadcast).await?.id;
710 std::thread::sleep(std::time::Duration::from_secs(2));
711
712 let broadcast = resend.broadcasts.get(&broadcast_id).await?;
714 assert_eq!(Some(subject.to_string()), broadcast.subject);
715
716 std::thread::sleep(std::time::Duration::from_secs(2));
717
718 let subject = "updated";
720 let opts = UpdateBroadcastOptions::new().with_subject(subject);
721 let _unused = resend.broadcasts.update(&broadcast_id, opts).await?;
722
723 let broadcast = resend.broadcasts.get(&broadcast_id).await?;
725 assert_eq!(Some(subject.to_string()), broadcast.subject);
726
727 let deleted = resend.broadcasts.delete(&broadcast_id).await?;
729 assert!(deleted);
730
731 Ok(())
732 }
733
734 #[test]
735 fn parse_broadcast_test() {
736 let data = r#"{
737 "object": "broadcast",
738 "id": "498ee8e4-7aa2-4eb5-9f04-4194848049d1",
739 "name": "Untitled",
740 "audience_id": "fd644f07-a05a-467e-9bae-23bb7c35766a",
741 "from": "Acme <onboarding@resend.dev>",
742 "subject": "Hello!",
743 "reply_to": [],
744 "preview_text": null,
745 "status": "scheduled",
746 "created_at": "2024-12-18 18:05:09.905933+00",
747 "scheduled_at": null,
748 "sent_at": null
749}"#;
750
751 let _parsed = serde_json::from_str::<Broadcast>(data).expect("Parsing failed");
752 }
753
754 #[test]
755 fn parse_cancel_broadcast_response_test() {
756 let data = r#"{
757 "object": "broadcast",
758 "id": "498ee8e4-7aa2-4eb5-9f04-4194848049d1"
759}"#;
760
761 let _parsed =
762 serde_json::from_str::<CancelBroadcastResponse>(data).expect("Parsing failed");
763 }
764
765 #[test]
766 fn parse_recipients_response_sent_test() {
767 let data = r#"{
768 "object": "list",
769 "has_more": false,
770 "data": [
771 {
772 "id": "b2Zmc2V0OjA",
773 "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
774 "email": "steve.wozniak@gmail.com"
775 },
776 {
777 "id": "b2Zmc2V0OjE",
778 "contact_id": null,
779 "email": "dana@example.com"
780 }
781 ]
782}"#;
783
784 let parsed =
785 serde_json::from_str::<crate::list_opts::ListResponse<BroadcastRecipient>>(data)
786 .expect("Parsing failed");
787
788 assert!(!parsed.has_more);
789 assert_eq!(parsed.len(), 2);
790 assert_eq!(
791 parsed[0].contact_id.as_deref(),
792 Some("e169aa45-1ecf-4183-9955-b1499d5701d3")
793 );
794 assert!(parsed[1].contact_id.is_none());
795 assert!(parsed[0].count.is_none());
796 assert!(parsed[0].bounce_type.is_none());
797 assert!(parsed[0].clicked_links.is_none());
798 }
799
800 #[test]
801 fn parse_recipients_response_opened_test() {
802 let data = r#"{
803 "id": "b2Zmc2V0OjA",
804 "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
805 "email": "steve.wozniak@gmail.com",
806 "count": 3
807}"#;
808
809 let parsed = serde_json::from_str::<BroadcastRecipient>(data).expect("Parsing failed");
810
811 assert_eq!(parsed.count, Some(3));
812 assert!(parsed.bounce_type.is_none());
813 assert!(parsed.clicked_links.is_none());
814 }
815
816 #[test]
817 fn parse_recipients_response_clicked_test() {
818 let data = r#"{
819 "id": "b2Zmc2V0OjA",
820 "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
821 "email": "carter@example.com",
822 "count": 3,
823 "clicked_links": [
824 { "url": "https://resend.com/pricing", "clicks": 2 },
825 { "url": "https://resend.com/docs", "clicks": 1 }
826 ]
827}"#;
828
829 let parsed = serde_json::from_str::<BroadcastRecipient>(data).expect("Parsing failed");
830
831 assert_eq!(parsed.count, Some(3));
832 let clicked_links = parsed.clicked_links.expect("clicked_links should be set");
833 assert_eq!(clicked_links.len(), 2);
834 assert_eq!(clicked_links[0].url, "https://resend.com/pricing");
835 assert_eq!(clicked_links[0].clicks, 2);
836 }
837
838 #[test]
839 fn parse_recipients_response_bounced_test() {
840 let data = r#"{
841 "id": "b2Zmc2V0OjA",
842 "contact_id": null,
843 "email": "bounced@example.com",
844 "bounce_type": "permanent"
845}"#;
846
847 let parsed = serde_json::from_str::<BroadcastRecipient>(data).expect("Parsing failed");
848
849 assert!(parsed.contact_id.is_none());
850 assert!(parsed.count.is_none());
851 assert!(parsed.clicked_links.is_none());
852 assert_eq!(
853 parsed.bounce_type,
854 Some(super::types::BroadcastRecipientBounceType::Permanent)
855 );
856 }
857
858 #[test]
859 fn serialize_list_recipients_options_test() {
860 use super::types::BroadcastRecipientBounceType;
861
862 let opts = ListRecipientsOptions::new(BroadcastRecipientEventType::Bounced)
863 .with_email("steve")
864 .with_bounce_type(BroadcastRecipientBounceType::Permanent)
865 .with_limit(10)
866 .list_after("cursor-123");
867
868 let json = serde_json::to_value(&opts).expect("Failed to serialize");
869
870 assert_eq!(json["type"], "bounced");
871 assert_eq!(json["email"], "steve");
872 assert_eq!(json["bounce_type"], "permanent");
873 assert_eq!(json["limit"], 10);
874 assert_eq!(json["after"], "cursor-123");
875 assert!(json.get("before").is_none() || json["before"].is_null());
876 }
877
878 #[tokio_shared_rt::test(shared = true)]
879 #[serial_test::serial]
880 #[cfg(not(feature = "blocking"))]
881 #[ignore = "requires RESEND_API_KEY and network access"]
882 async fn recipients_not_found() -> DebugResult<()> {
883 let resend = &*CLIENT;
884 std::thread::sleep(std::time::Duration::from_secs(1));
885
886 let list_opts = ListRecipientsOptions::new(BroadcastRecipientEventType::Sent);
887 let result = resend
888 .broadcasts
889 .recipients("00000000-0000-0000-0000-000000000000", list_opts)
890 .await;
891
892 assert!(result.is_err());
893
894 Ok(())
895 }
896
897 #[test]
898 fn parse_broadcast_clicked_links_test() {
899 let data = r#"{
900 "object": "list",
901 "has_more": true,
902 "data": [
903 {
904 "id": "b2Zmc2V0OjA",
905 "url": "https://resend.com/pricing",
906 "clicks": 42,
907 "unique_clicks": 30
908 },
909 {
910 "id": "b2Zmc2V0OjE",
911 "url": "https://resend.com/docs",
912 "clicks": 17,
913 "unique_clicks": 15
914 }
915 ]
916 }"#;
917
918 let _parsed =
919 serde_json::from_str::<crate::list_opts::ListResponse<BroadcastClickedLink>>(data)
920 .expect("Parsing failed");
921 }
922}