hermod_api/clients/
twilio.rs1use reqwest::Client;
3
4pub struct TwilioClient {
6 http_client: Client,
7 base_url: String,
8 account_sid: String,
9 auth_token: String,
10 from: String,
11}
12
13impl TwilioClient {
14 pub fn new(
16 base_url: String,
17 timeout: std::time::Duration,
18 account_sid: String,
19 auth_token: String,
20 from: String,
21 ) -> Self {
22 let http_client = Client::builder().timeout(timeout).build().unwrap();
23 Self {
24 http_client,
25 base_url,
26 account_sid,
27 auth_token,
28 from,
29 }
30 }
31
32 #[tracing::instrument(name = "clients::twilio::send_sms", skip(self))]
34 pub async fn send_sms(&self, to: String, message: String) -> Result<(), reqwest::Error> {
35 let url = format!("{}Accounts/{}/Messages", self.base_url, &self.account_sid);
36 let message = urlencoding::encode(&message);
37 let body = format!("Body={}&To={}&From={}", message, to, self.from);
38
39 self.http_client
40 .post(&url)
41 .header("Content-Type", "application/x-www-form-urlencoded")
42 .basic_auth(&self.account_sid, Some(&self.auth_token))
43 .body(body)
44 .send()
45 .await?
46 .error_for_status()?;
47 Ok(())
48 }
49
50 #[tracing::instrument(name = "clients::twilio::send_call", skip(self))]
52 pub async fn send_call(&self, to: String, message: String) -> Result<(), reqwest::Error> {
53 let url = format!("{}Accounts/{}/Calls.json", self.base_url, &self.account_sid);
54 let message = urlencoding::encode(&message);
55 let twiml = format!("<Response><Say>{}</Say></Response>", message);
56 let body = format!("Twiml={}&To={}&From={}", twiml, to, self.from);
57
58 self.http_client
59 .post(&url)
60 .header("Content-Type", "application/x-www-form-urlencoded")
61 .basic_auth(&self.account_sid, Some(&self.auth_token))
62 .body(body)
63 .send()
64 .await?
65 .error_for_status()?;
66 Ok(())
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use std::time::Duration;
73
74 use wiremock::{
75 matchers::{header, method, path},
76 Mock, MockServer, Request, ResponseTemplate,
77 };
78
79 use super::TwilioClient;
80
81 struct SendSmsBodyMatcher;
82
83 impl wiremock::Match for SendSmsBodyMatcher {
84 fn matches(&self, request: &Request) -> bool {
85 let result = String::from_utf8(request.body.clone());
86 if let Ok(body) = result {
87 body.contains(&format!("Body={}", urlencoding::encode("Hello, World!")))
88 && body.contains(&format!("To={}", "+12321231234"))
89 && body.contains(&format!("From={}", "+12321231234"))
90 } else {
91 false
92 }
93 }
94 }
95
96 struct SendCallBodyMatcher;
97
98 impl wiremock::Match for SendCallBodyMatcher {
99 fn matches(&self, request: &Request) -> bool {
100 let result = String::from_utf8(request.body.clone());
101 if let Ok(body) = result {
102 let message = urlencoding::encode("Hello, World!");
103 let twiml = format!("<Response><Say>{}</Say></Response>", message);
104 body.contains(&format!("Twiml={}", twiml))
105 && body.contains(&format!("To={}", "+12321231234"))
106 && body.contains(&format!("From={}", "+12321231234"))
107 } else {
108 false
109 }
110 }
111 }
112
113 fn twilio_client(base_url: String) -> TwilioClient {
114 TwilioClient::new(
115 base_url + "/",
116 Duration::from_secs(1),
117 "account_sid".to_string(),
118 "auth_token".to_string(),
119 "+12321231234".to_string(),
120 )
121 }
122
123 #[tokio::test]
124 async fn send_sms_sends_the_correct_http_request() {
125 let mock_server = MockServer::start().await;
126 let message = "Hello, World!";
127 let to = "+12321231234";
128 let client = twilio_client(mock_server.uri());
129 let url = format!("Accounts/{}/Messages", client.account_sid,);
130
131 Mock::given(header("Content-Type", "application/x-www-form-urlencoded"))
132 .and(path(url))
133 .and(method("POST"))
134 .and(SendSmsBodyMatcher)
135 .respond_with(ResponseTemplate::new(200))
136 .expect(1)
137 .mount(&mock_server)
138 .await;
139
140 let _ = client
141 .send_sms(to.to_string(), message.to_string())
142 .await
143 .unwrap();
144 }
145
146 #[tokio::test]
147 async fn send_call_sends_the_correct_http_request() {
148 let mock_server = MockServer::start().await;
149 let message = "Hello, World!";
150 let to = "+12321231234";
151 let client = twilio_client(mock_server.uri());
152 let url = format!("Accounts/{}/Calls.json", client.account_sid,);
153
154 Mock::given(header("Content-Type", "application/x-www-form-urlencoded"))
155 .and(path(url))
156 .and(method("POST"))
157 .and(SendCallBodyMatcher)
158 .respond_with(ResponseTemplate::new(200))
159 .expect(1)
160 .mount(&mock_server)
161 .await;
162
163 let _ = client
164 .send_call(to.to_string(), message.to_string())
165 .await
166 .unwrap();
167 }
168}