aboutsummaryrefslogtreecommitdiff
path: root/src/client.rs
blob: 2e38ba9ca2619331905efdf29cbffbf9c4dfd319 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};

#[derive(Debug, Clone)]
pub struct TwilioClient<'a> {
    // the HTTP client
    pub client: reqwest::blocking::Client,
    // the base URL
    pub base_url: reqwest::Url,
    // account SID
    pub account_sid: &'a String,
    // authentication token
    pub auth_token: &'a String,
}

const TWILIO_API_PATH: &str = "/2010-04-01/Accounts";

// list of possible values: https://www.twilio.com/docs/sms/api/message-resource#message-status-values
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[allow(non_camel_case_types)]
pub enum MessageStatus {
    accepted,
    scheduled,
    queued,
    sending,
    sent,
    receiving,
    received,
    delivered,
    undelivered,
    failed,
    read,
    canceled,
}

impl fmt::Display for MessageStatus {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

// response from twilio's API
#[derive(Deserialize, Debug, Clone)]
pub struct Message {
    pub status: MessageStatus,
}

// error from twilio's API
#[derive(Deserialize, Debug, Clone)]
pub struct Error {
    pub status: u16,
    pub code: Option<u16>,
    pub message: String,
    pub more_info: Option<String>,
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "({}) {}", self.status, self.message)?;
        Ok(())
    }
}

impl<'a> TwilioClient<'a> {
    pub fn new(account_sid: &'a String, auth_token: &'a String, base_url: reqwest::Url) -> Self {
        let client = reqwest::blocking::Client::builder()
            .user_agent("fcuny/send-sms")
            .build()
            .unwrap();

        Self {
            client,
            base_url,
            account_sid,
            auth_token,
        }
    }

    // send an SMS through twilio
    pub fn send(&self, message: crate::message::Message) -> Result<Message, Error> {
        let path = format!("{}/{}/Messages.json", TWILIO_API_PATH, self.account_sid);

        let mut url = self.base_url.clone();
        url.set_path(&path);

        let mut form = HashMap::new();
        form.insert("From", message.from);
        form.insert("To", message.to);
        form.insert("Body", message.body);

        let response = self
            .client
            .post(url.as_str())
            .basic_auth(self.account_sid, Some(self.auth_token))
            .form(&form)
            .send()
            .unwrap();

        match response.status() {
            reqwest::StatusCode::CREATED | reqwest::StatusCode::OK => {}
            _ => {
                let error_response: Error = response.json().unwrap();
                return Err(error_response);
            }
        };

        Ok(response.json().unwrap())
    }
}

#[cfg(test)]
mod test {
    use crate::client::TwilioClient;
    use crate::message::Message;
    use httpmock::prelude::*;
    use serde_json::json;

    fn get_message() -> Message {
        Message {
            to: Some("1234".to_string()),
            from: Some("1234".to_string()),
            body: Some("test".to_string()),
        }
    }

    #[test]
    fn test_send_message() {
        let server = MockServer::start();
        let mock = server.mock(|when, then| {
            when.method(POST)
                .path("/2010-04-01/Accounts/test/Messages.json");
            then.status(200).json_body(json!({"status": "queued"}));
        });

        let message = get_message();

        let account_sid = "test".to_string();
        let auth_token = "test".to_string();
        let client = TwilioClient::new(
            &account_sid,
            &auth_token,
            reqwest::Url::parse(&server.base_url()).unwrap(),
        );

        let result = client.send(message);
        mock.assert();
        let msg: crate::client::Message = result.unwrap();
        assert_eq!(msg.status, crate::client::MessageStatus::queued);
    }

    #[test]
    fn test_send_message_error() {
        let server = MockServer::start();
        let mock_invalid = server.mock(|when, then| {
            when.method(POST)
                .path("/2010-04-01/Accounts/test/Messages.json");
            then.status(400).json_body(json!({
                "status": 400,
                "message": "The 'From' number +15005550001 is not a valid phone number, shortcode, or alphanumeric sender ID",
            }));
        });

        let message = get_message();

        let account_sid = "test".to_string();
        let auth_token = "test".to_string();
        let client = TwilioClient::new(
            &account_sid,
            &auth_token,
            reqwest::Url::parse(&server.base_url()).unwrap(),
        );

        let result = client.send(message);
        mock_invalid.assert();
        assert!(matches!(result, Err(crate::client::Error { .. })));
    }
}