aboutsummaryrefslogblamecommitdiff
path: root/src/client.rs
blob: 2e38ba9ca2619331905efdf29cbffbf9c4dfd319 (plain) (tree)

















































































































































































                                                                                                                              
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 { .. })));
    }
}