summary history branches tags files
src/webapi/test.rs
#[cfg(test)]
mod tests {
    extern crate futures;
    extern crate hyper;
    extern crate fruitbasket;
    extern crate time;

    extern crate chrono;
    use webapi::chrono::TimeZone;

    use super::super::*;
    use super::super::super::SpotifyEndpoints;

    use std;
    use std::thread;
    use std::thread::sleep;
    use std::sync::atomic::AtomicBool;
    use std::sync::atomic::Ordering;
    use std::time::Duration;
    use std::sync::{Once, ONCE_INIT};

    use self::hyper::{Post, StatusCode};
    use self::hyper::server::{Service, Request, Response};
    use self::hyper::server::Http;
    use self::futures::Stream;
    use self::futures::Future;

    static START: Once = ONCE_INIT;

    lazy_static! {
        static ref WEBSERVER_STARTED: AtomicBool = AtomicBool::new(false);
    }

    pub const TEST_API: SpotifyEndpoints = SpotifyEndpoints {
        scopes: "user-read-private streaming user-read-playback-state",
        scopes_version: 1,
        authorize: "http://127.0.0.1:9799/en/authorize",
        token: "http://127.0.0.1:9799/api/token",
        devices: "http://127.0.0.1:9799/v1/me/player/devices",
        player_state: "http://127.0.0.1:9799/v1/me/player",
        play: "http://127.0.0.1:9799/v1/me/player/play",
        pause: "http://127.0.0.1:9799/v1/me/player/pause",
        next: "http://127.0.0.1:9799/v1/me/player/next",
        previous: "http://127.0.0.1:9799/v1/me/player/previous",
        seek: "http://127.0.0.1:9799/v1/me/player/seek",
        volume: "http://127.0.0.1:9799/v1/me/player/volume",
        shuffle: "http://127.0.0.1:9799/v1/me/player/shuffle",
        repeat: "http://127.0.0.1:9799/v1/me/player/repeat",
        player: "http://127.0.0.1:9799/v1/me/player",
        add_to_playlist: "http://127.0.0.1:9799/v1/users",
    };

    pub const BAD_TEST_API: SpotifyEndpoints = SpotifyEndpoints {
        scopes: "user-read-private streaming user-read-playback-state",
        scopes_version: 1,
        authorize: "http://127.0.0.1:11111/en/authorize",
        token: "http://127.0.0.1:11111/api/token",
        devices: "http://127.0.0.1:11111/v1/me/player/devices",
        player_state: "http://127.0.0.1:11111/v1/me/player",
        play: "http://127.0.0.1:11111/v1/me/player/play",
        pause: "http://127.0.0.1:11111/v1/me/player/pause",
        next: "http://127.0.0.1:11111/v1/me/player/next",
        previous: "http://127.0.0.1:11111/v1/me/player/previous",
        seek: "http://127.0.0.1:11111/v1/me/player/seek",
        volume: "http://127.0.0.1:11111/v1/me/player/volume",
        shuffle: "http://127.0.0.1:11111/v1/me/player/shuffle",
        repeat: "http://127.0.0.1:11111/v1/me/player/repeat",
        player: "http://127.0.0.1:11111/v1/me/player",
        add_to_playlist: "http://127.0.0.1:11111/v1/users",
    };

    /// Macro to parse the body of a POST request and send a response.
    ///
    /// There's probably a "body.to_string()" function somewhere.  I didn't find it.
    /// So instead there's this unreadable, overly complicated bullshit.
    ///
    /// $body_in: a POST body (hyper::Body) from a received POST request
    /// $pairs_out: the name of the key/value pair variable provided to the $block_in
    /// $block_in: a block of code to be executed, with $pairs_out in scope, that evaluates
    ///            to tuple (status_code: StatusCode, body: &str) to send as a response.
    macro_rules! post {
    ($body_in:ident, $pairs_out:ident, $block_in:block) => {
        {
            Box::new(
            // Read chunks from user provided body var $body_in
            $body_in.fold(vec![], |mut acc, chunk| {
                acc.extend(chunk);
                Ok::<_, hyper::Error>(acc)
            }).and_then(move |bytes| {
                // [u8] -> String
                let post_data: String = std::str::from_utf8(&bytes).unwrap().to_string();;
                // Split on & to get ["key=value"...]
                let pairs = post_data.split("&");
                // Split on = to get [[key,value]...], put in user provided var name $pairs_out
                let $pairs_out = pairs.map(|pair| pair.split("=").collect::<Vec<&str>>()).collect::<Vec<Vec<&str>>>();
                // User provided block takes $pairs_out and returns response string
                let (code, response) = $block_in;
                let res = Response::new();
                Ok(res.with_status(code).with_body(response))
            }))
        }
    };
    }

    fn token_response(pairs: &Vec<Vec<&str>>) -> (StatusCode, String) {
        let mut resp = String::new();
        let mut code = StatusCode::Ok;
        resp.push_str("{");
        resp.push_str(r#""access_token": "valid_access_code","#);
        resp.push_str(r#""token_type": "Bearer","#);
        resp.push_str(r#""scope": "user-read-private user-read-email","#);
        resp.push_str(r#""expires_in": 3600"#);
        resp.push_str("}");
        for pair in pairs {
            let (key,value) = (pair[0], pair[1]);
            if key == "refresh_token" && value == "error" {
                code = StatusCode::Forbidden;
            }
        }
        (code, resp)
    }

    fn init() {
        START.call_once(|| {
            #[derive(Clone, Copy)]
            struct Webapi;
            impl Service for Webapi {
                type Request = Request;
                type Response = Response;
                type Error = hyper::Error;
                type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
                fn call(&self, req: Request) -> Self::Future {
                    let (method, uri, _, _headers, body) = req.deconstruct();
                    match(method, uri.path()) {
                        (Post, "/api/token") => post!(body, pairs, { token_response(&pairs) }),
                        _ => Box::new(futures::future::ok(Response::new().with_status(StatusCode::NotFound))),
                    }
                }
            }
            thread::spawn(move || {
                let addr = "127.0.0.1:9799".parse().unwrap();
                let server = Http::new().bind(&addr, || Ok(Webapi)).unwrap();
                WEBSERVER_STARTED.store(true, Ordering::Relaxed);
                server.run().unwrap();
            });
            // Give it some time to really come up.  It would be nicer to
            // actually try to connect to it.
            sleep(Duration::from_millis(500));
        });
        while !WEBSERVER_STARTED.load(Ordering::Relaxed) {
            sleep(Duration::from_millis(100));
        }
    }

    #[test]
    fn test_refresh_oauth_tokens_no_connection() {
        let now = time::now_utc().to_timespec().sec as u64;
        let spotify = SpotifyConnectr::new()
            .with_api(BAD_TEST_API)
            .with_oauth_tokens("access", "refresh", now + 3600)
            .build()
            .unwrap();
        let res = spotify.refresh_oauth_tokens();
        // Unlock webserver init so all other tests can run
        WEBSERVER_STARTED.store(true, Ordering::Relaxed);
        assert!(res.is_some());
    }

    #[test]
    fn test_refresh_oauth_tokens_pass() {
        init();
        let now = time::now_utc().to_timespec().sec as u64;
        let spotify = SpotifyConnectr::new()
            .with_api(TEST_API)
            .with_oauth_tokens("access", "refresh", now + 3600)
            .build()
            .unwrap();
        match spotify.refresh_oauth_tokens() {
            Some((access,expires)) => {
                assert_eq!(access, "valid_access_code");
                assert_eq!(expires, 3600);
            },
            None => { assert!(false) },
        }
    }

    #[test]
    fn test_refresh_oauth_tokens_error_status() {
        init();
        let now = time::now_utc().to_timespec().sec as u64;
        let mut spotify = SpotifyConnectr::new()
            .with_api(TEST_API)
            .with_oauth_tokens("access", "refresh", now + 3600)
            .build()
            .unwrap();
        spotify.refresh_token = Some("error".to_string());
        match spotify.refresh_oauth_tokens() {
            Some(_) => { assert!(false) },
            None => { },
        }
    }

    fn build_alarm_entry(time: &str, repeat: AlarmRepeat, now: DateTime<Local>) -> AlarmEntry {
        return AlarmEntry {
            time: time.to_string(),
            repeat: repeat,
            context: PlayContext::new().build(),
            device: "12345".to_string(),
            now: Some(now),
        }
    }

    #[test]
    fn test_alarm_scheduler() {
        let now = time::now_utc().to_timespec().sec as u64;
        let mut spotify = SpotifyConnectr::new()
            .with_api(TEST_API)
            .with_oauth_tokens("access", "refresh", now + 3600)
            .build()
            .unwrap();

        // From a Friday morning
        let today = Local.ymd(2017, 06, 16).and_hms_milli(9, 00, 00, 0);

        // Daily, later the same day
        let entry = build_alarm_entry("21:00", AlarmRepeat::Daily, today.clone());
        let alarm = spotify.schedule_alarm(entry).unwrap();
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-16 21:00:00");

        // Daily, the next day
        let entry = build_alarm_entry("08:00", AlarmRepeat::Daily, today.clone());
        let alarm = spotify.schedule_alarm(entry).unwrap();
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-17 08:00:00");

        // Weekends, the next day
        let entry = build_alarm_entry("08:00", AlarmRepeat::Weekends, today.clone());
        let alarm = spotify.schedule_alarm(entry).unwrap();
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-17 08:00:00");

        // Weekdays, the next week
        let entry = build_alarm_entry("08:00", AlarmRepeat::Weekdays, today.clone());
        let alarm = spotify.schedule_alarm(entry).unwrap();
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-19 08:00:00");

        // From a Wednesday night
        let today = Local.ymd(2017, 06, 21).and_hms_milli(21, 00, 00, 0);

        // Daily, later the same day
        let entry = build_alarm_entry("23:00", AlarmRepeat::Daily, today.clone());
        let alarm = spotify.schedule_alarm(entry).unwrap();
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-21 23:00:00");

        // Daily, the next day
        let entry = build_alarm_entry("00:00", AlarmRepeat::Daily, today.clone());
        let alarm = spotify.schedule_alarm(entry).unwrap();
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-22 00:00:00");

        // Weekends, 3 days later
        let entry = build_alarm_entry("08:00", AlarmRepeat::Weekends, today.clone());
        let alarm = spotify.schedule_alarm(entry).unwrap();
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-24 08:00:00");

        // Weekdays, the next day
        let entry = build_alarm_entry("08:00", AlarmRepeat::Weekdays, today.clone());
        let alarm = spotify.schedule_alarm(entry).unwrap();
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-22 08:00:00");
        // Disable/re-enable and verify it responds correctly
        assert_eq!(spotify.alarm_enabled(alarm), true);
        assert!(spotify.alarm_disable(alarm).is_ok());
        assert_eq!(spotify.alarm_enabled(alarm), false);
        assert!(spotify.alarm_time(alarm).is_err());
        assert!(spotify.alarm_reschedule(alarm).is_ok());
        assert_eq!(spotify.alarm_time(alarm).unwrap().format("%Y-%m-%d %H:%M:%S").to_string(), "2017-06-22 08:00:00");
    }
}