melcloud-api 0.1.1

Provides a rust interface to the undocumented melcloud API
Documentation
// A Session is where everything begins.  My plan (for now) is that we go via the session
// for everything, but we will see how the API develops.
use crate::api::types::Config;
use crate::api::errors;
use reqwest;
use crate::api::json::{MelcloudLoginResponse};
use crate::api::types::devices::Devices;


#[derive(Debug)]
pub struct Session {
    pub config: Config,
    pub api_key: String
}

impl Session {
    /// Starts a new session
    pub fn start(config: Config) -> Result<Session, errors::ApiError> {
        let request_url = format!("{api_base}/Login/ClientLogin", api_base = &config.api_url);
        let post_body = format!("{{\"Email\":\"{email}\",\"Password\":\"{password}\",\"Language\":0,\"AppVersion\":\"1.18.5.1\",\"Persist\":true,\"CaptchaResponse\":null}}", email = config.api_username, password = config.api_password);
        let result = reqwest::Client::new()
            .post(&request_url)
            .body(post_body)
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .send();
        match result {
            Ok(mut resp) => {
                match resp.json() {
                    Ok(json) => Session::parse_new_session_response(config, json),
                    Err(err) => {
                        println!("Err is {:?}", err);
                        Err(errors::ApiError::InvalidLoginResponse)
                    }
                }
            },
            Err(err) => Err(errors::ApiError::LoginFailure)
        }
    }

    pub fn devices(&self) -> Devices {
        Devices::new(self)
    }

    fn parse_new_session_response(config: Config, message: MelcloudLoginResponse) -> Result<Session, errors::ApiError> {
        match message {
            MelcloudLoginResponse::Success {login_data} => {
                Ok(Session {
                    config: config,
                    api_key: login_data.context_key
                })
            },
            MelcloudLoginResponse::AccessDenied {error_id} => {
                Err(errors::ApiError::LoginFailure)
            }
        }
    }

}

#[cfg(test)]
mod tests {
    use mockito::{mock, Matcher};
    use crate::api::types::Config;
    use crate::api::types::Session;
    use crate::api::errors;

    #[test]
    fn test_start_session() {
        // Arrange - Create a config and setup a mock server
        let config = Config::new(&mockito::server_url(), "testuser", "testpassword");
        let m = mock("POST", "/Login/ClientLogin")
            .with_status(200)
            .with_body("{\"ErrorId\":null,\"ErrorMessage\":null,\"LoginStatus\":0,\"UserId\":0,\"RandomKey\":null,\"AppVersionAnnouncement\":null,\"LoginData\":{\"ContextKey\":\"C71933C57FB04358B6750ED77D79AA\",\"Client\":128538,\"Terms\":1199,\"AL\":1,\"ML\":0,\"CMI\":true,\"IsStaff\":false,\"CUTF\":false,\"CAA\":false,\"ReceiveCountryNotifications\":false,\"ReceiveAllNotifications\":false,\"CACA\":false,\"CAGA\":false,\"MaximumDevices\":10,\"ShowDiagnostics\":false,\"Language\":0,\"Country\":237,\"RealClient\":0,\"Name\":\"Gary Taylor\",\"UseFahrenheit\":false,\"Duration\":525600,\"Expiry\":\"2020-10-14T13:54:42.653\",\"CMSC\":false,\"PartnerApplicationVersion\":null,\"EmailSettingsReminderShown\":true,\"EmailUnitErrors\":1,\"EmailCommsErrors\":1,\"IsImpersonated\":false,\"LanguageCode\":\"en\",\"CountryName\":\"United Kingdom\",\"CurrencySymbol\":\"£\",\"SupportEmailAddress\":\"melcloud.support@meuk.mee.com \",\"DateSeperator\":\"/\",\"TimeSeperator\":\":\",\"AtwLogoFile\":\"ecodan_logo.png\",\"DECCReport\":true,\"CSVReport1min\":true,\"HidePresetPanel\":false,\"EmailSettingsReminderRequired\":false,\"TermsText\":null,\"MapView\":false,\"MapZoom\":0,\"MapLongitude\":-133.082129213169600,\"MapLatitude\":52.621242998717900},\"ListPendingInvite\":[],\"ListOwnershipChangeRequest\":[],\"ListPendingAnnouncement\":[],\"LoginMinutes\":0,\"LoginAttempts\":0}")
            .create();

        // Act - Start the session
        let result = Session::start(config);

        // Assert - Make sure the result contains the api key
        m.assert();
        match result {
            Ok(session) => assert!(session.api_key == "C71933C57FB04358B6750ED77D79AA", "The api key was wrong"),
            Err(err) => assert!(false, "Something went wrong"),
        }
    }

    #[test]
    fn test_start_session_wrong_credentials() {
        // Arrange - Create a config and setup a mock server
        let config = Config::new(&mockito::server_url(), "testuser", "testpassword");
        let m = mock("POST", "/Login/ClientLogin")
            .with_status(200)
            .with_body("{\"ErrorId\":1,\"ErrorMessage\":null,\"LoginStatus\":0,\"UserId\":0,\"RandomKey\":null,\"AppVersionAnnouncement\":null,\"LoginData\":null,\"ListPendingInvite\":null,\"ListOwnershipChangeRequest\":null,\"ListPendingAnnouncement\":null,\"LoginMinutes\":0,\"LoginAttempts\":1}")
            .create();

        // Act - Start the session
        let result = Session::start(config);

        // Assert - Make sure the result contains the api key
        m.assert();
        match result {
            Ok(session) => assert!(false, "Must return an error"),
            Err(errors::ApiError::LoginFailure) => assert!(true),
            Err(_) => assert!(false, "Must return a Login Failure")
        }
    }
}