diff --git a/.gitignore b/.gitignore index d629c867..d049dbbd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ Cargo.lock .DS_Store .spotify_cache/ **/target/ +/.test_read_token_cache.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f42e549f..4c1dec13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ If we missed any change or there's something you'd like to discuss about this ve + `ExplicitContent` + Fix broken model links refering to Spotify documentation - ([#188](https://github.com/ramsayleung/rspotify/pull/188)) Replace html links with intra-documentation links +- ([#189](https://github.com/ramsayleung/rspotify/pull/189)) Add `scopes!` macro to generate scope for `Token` from string literal **Breaking changes:** - `SpotifyClientCredentials` has been renamed to `Credentials` ([#129](https://github.com/ramsayleung/rspotify/pull/129)), and its members `client_id` and `client_secret` to `id` and `secret`, respectively. diff --git a/examples/current_user_recently_played.rs b/examples/current_user_recently_played.rs index c227e5a1..3001e137 100644 --- a/examples/current_user_recently_played.rs +++ b/examples/current_user_recently_played.rs @@ -1,7 +1,6 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; - -use std::collections::HashSet; +use rspotify::scopes; #[tokio::main] async fn main() { @@ -31,9 +30,10 @@ async fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let mut scopes = HashSet::new(); - scopes.insert("user-read-recently-played".to_owned()); - let oauth = OAuthBuilder::from_env().scope(scopes).build().unwrap(); + let oauth = OAuthBuilder::from_env() + .scope(scopes!("user-read-recently-played")) + .build() + .unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/oauth_tokens.rs b/examples/oauth_tokens.rs index fa806032..010e515a 100644 --- a/examples/oauth_tokens.rs +++ b/examples/oauth_tokens.rs @@ -7,6 +7,7 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use rspotify::scopes; #[tokio::main] async fn main() { @@ -18,17 +19,26 @@ async fn main() { let creds = CredentialsBuilder::from_env().build().unwrap(); // Using every possible scope - let scope = "user-read-email user-read-private user-top-read \ - user-read-recently-played user-follow-read user-library-read \ - user-read-currently-playing user-read-playback-state \ - user-read-playback-position playlist-read-collaborative \ - playlist-read-private user-follow-modify user-library-modify \ - user-modify-playback-state playlist-modify-public \ - playlist-modify-private ugc-image-upload"; - let oauth = OAuthBuilder::from_env() - .scope(scope.split_whitespace().map(|x| x.to_owned()).collect()) - .build() - .unwrap(); + let scope = scopes!( + "user-read-email", + "user-read-private", + "user-top-read", + "user-read-recently-played", + "user-follow-read", + "user-library-read", + "user-read-currently-playing", + "user-read-playback-state", + "user-read-playback-position", + "playlist-read-collaborative", + "playlist-read-private", + "user-follow-modify", + "user-library-modify", + "user-modify-playback-state", + "playlist-modify-public", + "playlist-modify-private", + "ugc-image-upload" + ); + let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/ureq/device.rs b/examples/ureq/device.rs index c68b046b..fb322050 100644 --- a/examples/ureq/device.rs +++ b/examples/ureq/device.rs @@ -1,5 +1,6 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use rspotify::scope; use std::collections::HashSet; @@ -30,9 +31,10 @@ fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let mut scope = HashSet::new(); - scope.insert("user-read-playback-state".to_owned()); - let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); + let oauth = OAuthBuilder::from_env() + .scope(scopes!("user-read-playback-state")) + .build() + .unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/ureq/me.rs b/examples/ureq/me.rs index d6e39a3c..4667ac1d 100644 --- a/examples/ureq/me.rs +++ b/examples/ureq/me.rs @@ -1,5 +1,6 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use rspotify::scope; use std::collections::HashSet; @@ -30,9 +31,10 @@ fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let mut scope = HashSet::new(); - scope.insert("user-read-playback-state".to_owned()); - let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); + let oauth = OAuthBuilder::from_env() + .scope(scopes!("user-read-playback-state")) + .build() + .unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/ureq/search.rs b/examples/ureq/search.rs index e5851da6..1f939f23 100644 --- a/examples/ureq/search.rs +++ b/examples/ureq/search.rs @@ -1,6 +1,7 @@ use rspotify::client::SpotifyBuilder; use rspotify::model::{Country, Market, SearchType}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use rspotify::scope; use std::collections::HashSet; @@ -31,9 +32,10 @@ fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let mut scope = HashSet::new(); - scope.insert("user-read-playback-state".to_owned()); - let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); + let oauth = OAuthBuilder::from_env() + .scope(scopes!("user-read-playback-state")) + .build() + .unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/ureq/seek_track.rs b/examples/ureq/seek_track.rs index 76e680b9..8dacf63e 100644 --- a/examples/ureq/seek_track.rs +++ b/examples/ureq/seek_track.rs @@ -1,5 +1,6 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use rspotify::scope; use std::collections::HashSet; @@ -30,9 +31,10 @@ fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let mut scope = HashSet::new(); - scope.insert("user-read-playback-state".to_owned()); - let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); + let oauth = OAuthBuilder::from_env() + .scope(scopes!("user-read-playback-state")) + .build() + .unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/webapp/src/main.rs b/examples/webapp/src/main.rs index 078532ce..14525b82 100644 --- a/examples/webapp/src/main.rs +++ b/examples/webapp/src/main.rs @@ -16,6 +16,7 @@ use rocket_contrib::json::JsonValue; use rocket_contrib::templates::Template; use rspotify::client::{ClientError, SpotifyBuilder}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; +use rspotify::scope; use std::fs; use std::{ @@ -75,10 +76,10 @@ fn check_cache_path_exists(cookies: &Cookies) -> (bool, PathBuf) { fn init_spotify() -> SpotifyBuilder { // Please notice that protocol of redirect_uri, make sure it's http // (or https). It will fail if you mix them up. - let scope = "user-read-currently-playing playlist-modify-private"; + let scope = scopes!("user-read-currently-playing", "playlist-modify-private"); let oauth = OAuthBuilder::default() .redirect_uri("http://localhost:8000/callback") - .scope(scope.split_whitespace().map(|x| x.to_owned()).collect()) + .scope(scope) .build() .unwrap(); diff --git a/examples/with_refresh_token.rs b/examples/with_refresh_token.rs index 8106943c..8be37bec 100644 --- a/examples/with_refresh_token.rs +++ b/examples/with_refresh_token.rs @@ -18,6 +18,7 @@ use rspotify::client::{Spotify, SpotifyBuilder}; use rspotify::model::Id; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use rspotify::scopes; // Sample request that will follow some artists, print the user's // followed artists, and then unfollow the artists. @@ -57,11 +58,8 @@ async fn main() { // The default credentials from the `.env` file will be used by default. let creds = CredentialsBuilder::from_env().build().unwrap(); - let scope = "user-follow-read user-follow-modify"; - let oauth = OAuthBuilder::from_env() - .scope(scope.split_whitespace().map(|x| x.to_owned()).collect()) - .build() - .unwrap(); + let scope = scopes!("user-follow-read user-follow-modify"); + let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds.clone()) .oauth(oauth.clone()) diff --git a/src/client.rs b/src/client.rs index 27781d1a..b5320ce2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2044,7 +2044,7 @@ fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>) -> Str } #[cfg(test)] -mod tests { +mod test { use super::*; #[test] @@ -2054,4 +2054,25 @@ mod tests { let code = spotify.parse_response_code(url).unwrap(); assert_eq!(code, "AQD0yXvFEOvw"); } + + #[test] + fn test_append_device_id_without_question_mark() { + let path = "me/player/play"; + let device_id = Some("fdafdsadfa".to_owned()); + let spotify = SpotifyBuilder::default().build().unwrap(); + let new_path = spotify.append_device_id(path, device_id); + assert_eq!(new_path, "me/player/play?device_id=fdafdsadfa"); + } + + #[test] + fn test_append_device_id_with_question_mark() { + let path = "me/player/shuffle?state=true"; + let device_id = Some("fdafdsadfa".to_owned()); + let spotify = SpotifyBuilder::default().build().unwrap(); + let new_path = spotify.append_device_id(path, device_id); + assert_eq!( + new_path, + "me/player/shuffle?state=true&device_id=fdafdsadfa" + ); + } } diff --git a/src/http/mod.rs b/src/http/mod.rs index a4d22165..d1fd790a 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -232,3 +232,69 @@ impl Spotify { self.delete(url, Some(&headers), payload).await } } + +#[cfg(test)] +mod test { + use super::*; + use crate::client::SpotifyBuilder; + use crate::oauth2::TokenBuilder; + use crate::scopes; + use chrono::prelude::*; + use chrono::Duration; + + #[test] + fn test_bearer_auth() { + let access_token = "access_token"; + let tok = TokenBuilder::default() + .access_token(access_token) + .build() + .unwrap(); + let (auth, value) = headers::bearer_auth(&tok); + assert_eq!(auth, "authorization"); + assert_eq!(value, "Bearer access_token"); + } + + #[test] + fn test_basic_auth() { + let (auth, value) = headers::basic_auth("ramsay", "123456"); + assert_eq!(auth, "authorization"); + assert_eq!(value, "Basic cmFtc2F5OjEyMzQ1Ng=="); + } + + #[test] + fn test_endpoint_url() { + let spotify = SpotifyBuilder::default().build().unwrap(); + assert_eq!( + spotify.endpoint_url("me/player/play"), + "https://api.spotify.com/v1/me/player/play" + ); + assert_eq!( + spotify.endpoint_url("http://api.spotify.com/v1/me/player/play"), + "http://api.spotify.com/v1/me/player/play" + ); + assert_eq!( + spotify.endpoint_url("https://api.spotify.com/v1/me/player/play"), + "https://api.spotify.com/v1/me/player/play" + ); + } + + #[test] + fn test_auth_headers() { + let tok = TokenBuilder::default() + .access_token("test-access_token") + .expires_in(Duration::seconds(1)) + .expires_at(Utc::now()) + .scope(scopes!("playlist-read-private")) + .refresh_token("...") + .build() + .unwrap(); + + let spotify = SpotifyBuilder::default().token(tok).build().unwrap(); + + let headers = spotify.auth_headers().unwrap(); + assert_eq!( + headers.get("authorization"), + Some(&"Bearer test-access_token".to_owned()) + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index f0472ba2..c388e237 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -165,26 +165,15 @@ pub mod model; #[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] pub mod oauth2; +#[macro_use] +mod macros; + #[cfg(all(feature = "client-reqwest", feature = "client-ureq"))] compile_error!( "`client-reqwest` and `client-ureq` features cannot both be enabled at the same time, \ if you want to use `client-ureq` you need to set `default-features = false`" ); -#[doc(hidden)] -mod macros { - /// Reduce boilerplate when inserting new elements in a JSON object. - #[macro_export] - macro_rules! json_insert { - ($json:expr, $p1:expr, $p2:expr) => { - $json - .as_object_mut() - .unwrap() - .insert($p1.to_string(), json!($p2)) - }; - } -} - /// Generate `length` random chars pub(in crate) fn generate_random_string(length: usize) -> String { let alphanum: &[u8] = @@ -197,3 +186,37 @@ pub(in crate) fn generate_random_string(length: usize) -> String { .map(|byte| alphanum[*byte as usize % range] as char) .collect() } + +#[cfg(test)] +mod test { + use super::{generate_random_string, json_insert, scopes}; + use serde_json::json; + use std::collections::HashSet; + + #[test] + fn test_hashset() { + let scope = scopes!("hello", "world", "foo", "bar"); + assert_eq!(scope.len(), 4); + assert!(scope.contains(&"hello".to_owned())); + assert!(scope.contains(&"world".to_owned())); + assert!(scope.contains(&"foo".to_owned())); + assert!(scope.contains(&"bar".to_owned())); + } + + #[test] + fn test_generate_random_string() { + let mut containers = HashSet::new(); + for _ in 1..101 { + containers.insert(generate_random_string(10)); + } + assert_eq!(containers.len(), 100); + } + + #[test] + fn test_json_insert() { + let mut params = json!({}); + let name = "ramsay"; + json_insert!(params, "name", name); + assert_eq!(params["name"], name); + } +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 00000000..fd321186 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,42 @@ +/// Create a **Hashset** from a list of &str(which will be converted to +/// String internally), be used to create scope +/// for (Token)[crate::oauth2::Token] +/// Example +/// ``` +/// use rspotify::oauth2::TokenBuilder; +/// use rspotify::scopes; +/// use std::collections::HashSet; +/// use chrono::prelude::*; +/// use chrono::Duration; +/// +/// let scope: HashSet = scopes!("playlist-read-private", "playlist-read-collaborative"); +/// let tok = TokenBuilder::default() +/// .access_token("test-access_token") +/// .expires_in(Duration::seconds(1)) +/// .expires_at(Utc::now()) +/// .scope(scope) +/// .refresh_token("...") +/// .build() +/// .unwrap(); +/// ``` +#[macro_export] +macro_rules! scopes { + ($($key:expr),*) => {{ + let mut container = ::std::collections::HashSet::new(); + $( + container.insert($key.to_owned()); + )* + container + }}; +} +/// Reduce boilerplate when inserting new elements in a JSON object. +#[doc(hidden)] +#[macro_export] +macro_rules! json_insert { + ($json:expr, $p1:expr, $p2:expr) => { + $json + .as_object_mut() + .unwrap() + .insert($p1.to_string(), json!($p2)) + }; +} diff --git a/src/oauth2.rs b/src/oauth2.rs index 9cd08985..b8087e96 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -87,6 +87,7 @@ pub struct Token { pub refresh_token: Option, /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/scopes/) /// which have been granted for this `access_token` + /// You could use macro [scopes!](crate::scopes) to build it at compile time easily #[builder(default)] #[serde(default, with = "space_separated_scope")] pub scope: HashSet, @@ -169,6 +170,7 @@ pub struct OAuth { /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) #[builder(setter(into), default = "generate_random_string(16)")] pub state: String, + /// You could use macro [scopes!](crate::scopes) to build it at compile time easily #[builder(default)] pub scope: HashSet, #[builder(setter(into, strip_option), default)] @@ -431,140 +433,3 @@ impl Spotify { Ok(code) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::client::SpotifyBuilder; - use url::Url; - - use chrono::Duration; - use std::collections::HashSet; - use std::fs; - use std::io::Read; - use std::thread::sleep; - - #[test] - fn test_get_authorize_url() { - let scope = "playlist-read-private"; - - let oauth = OAuthBuilder::default() - .state("fdsafdsfa") - .redirect_uri("localhost") - .scope(scope.split_whitespace().map(|x| x.to_owned()).collect()) - .build() - .unwrap(); - - let creds = CredentialsBuilder::default() - .id("this-is-my-client-id") - .secret("this-is-my-client-secret") - .build() - .unwrap(); - - let spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); - - let authorize_url = spotify.get_authorize_url(false).unwrap(); - let hash_query: HashMap<_, _> = Url::parse(&authorize_url) - .unwrap() - .query_pairs() - .into_owned() - .collect(); - - assert_eq!(hash_query.get("client_id").unwrap(), "this-is-my-client-id"); - assert_eq!(hash_query.get("response_type").unwrap(), "code"); - assert_eq!(hash_query.get("redirect_uri").unwrap(), "localhost"); - assert_eq!(hash_query.get("scope").unwrap(), "playlist-read-private"); - assert_eq!(hash_query.get("state").unwrap(), "fdsafdsfa"); - } - #[test] - fn test_write_token() { - let now: DateTime = Utc::now(); - let scope = "playlist-read-private playlist-read-collaborative \ - playlist-modify-public playlist-modify-private \ - streaming ugc-image-upload user-follow-modify \ - user-follow-read user-library-read user-library-modify \ - user-read-private user-read-birthdate user-read-email \ - user-top-read user-read-playback-state user-modify-playback-state \ - user-read-currently-playing user-read-recently-played"; - let scope = scope - .split_whitespace() - .map(|x| x.to_owned()) - .collect::>(); - - let tok = TokenBuilder::default() - .access_token("test-access_token") - .expires_in(Duration::seconds(3600)) - .expires_at(now) - .scope(scope.clone()) - .refresh_token("...") - .build() - .unwrap(); - - let spotify = SpotifyBuilder::default() - .token(tok.clone()) - .build() - .unwrap(); - - let tok_str = serde_json::to_string(&tok).unwrap(); - spotify.write_token_cache().unwrap(); - - let mut file = fs::File::open(&spotify.cache_path).unwrap(); - let mut tok_str_file = String::new(); - file.read_to_string(&mut tok_str_file).unwrap(); - - assert_eq!(tok_str, tok_str_file); - let tok_from_file: Token = serde_json::from_str(&tok_str_file).unwrap(); - assert_eq!(tok_from_file.scope, scope); - assert_eq!(tok_from_file.expires_in, Duration::seconds(3600)); - assert_eq!(tok_from_file.expires_at.unwrap(), now); - } - - #[test] - fn test_token_is_expired() { - let scope = "playlist-read-private playlist-read-collaborative \ - playlist-modify-public playlist-modify-private streaming \ - ugc-image-upload user-follow-modify user-follow-read \ - user-library-read user-library-modify user-read-private \ - user-read-birthdate user-read-email user-top-read \ - user-read-playback-state user-modify-playback-state \ - user-read-currently-playing user-read-recently-played"; - let scope = scope.split_whitespace().map(|x| x.to_owned()).collect(); - - let tok = TokenBuilder::default() - .access_token("test-access_token") - .expires_in(Duration::seconds(1)) - .expires_at(Utc::now()) - .scope(scope) - .refresh_token("...") - .build() - .unwrap(); - assert!(!tok.is_expired()); - sleep(std::time::Duration::from_secs(2)); - assert!(tok.is_expired()); - } - - #[test] - fn test_parse_response_code() { - let spotify = SpotifyBuilder::default().build().unwrap(); - - let url = "http://localhost:8888/callback"; - let code = spotify.parse_response_code(url); - assert_eq!(code, None); - - let url = "http://localhost:8888/callback?code=AQD0yXvFEOvw"; - let code = spotify.parse_response_code(url); - assert_eq!(code, Some("AQD0yXvFEOvw".to_string())); - - let url = "http://localhost:8888/callback?code=AQD0yXvFEOvw&state=sN#_=_"; - let code = spotify.parse_response_code(url); - assert_eq!(code, Some("AQD0yXvFEOvw".to_string())); - - let url = "http://localhost:8888/callback?state=sN&code=AQD0yXvFEOvw#_=_"; - let code = spotify.parse_response_code(url); - assert_eq!(code, Some("AQD0yXvFEOvw".to_string())); - } -} diff --git a/tests/test_oauth2.rs b/tests/test_oauth2.rs new file mode 100644 index 00000000..8002a044 --- /dev/null +++ b/tests/test_oauth2.rs @@ -0,0 +1,170 @@ +use chrono::prelude::*; +use chrono::Duration; +use maybe_async::maybe_async; +use rspotify::client::SpotifyBuilder; +use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, Token, TokenBuilder}; +use rspotify::scopes; +use std::{collections::HashMap, fs, io::Read, path::PathBuf, thread::sleep}; +use url::Url; +mod common; + +use common::maybe_async_test; + +#[test] +fn test_get_authorize_url() { + let oauth = OAuthBuilder::default() + .state("fdsafdsfa") + .redirect_uri("localhost") + .scope(scopes!("playlist-read-private")) + .build() + .unwrap(); + + let creds = CredentialsBuilder::default() + .id("this-is-my-client-id") + .secret("this-is-my-client-secret") + .build() + .unwrap(); + + let spotify = SpotifyBuilder::default() + .credentials(creds) + .oauth(oauth) + .build() + .unwrap(); + + let authorize_url = spotify.get_authorize_url(false).unwrap(); + let hash_query: HashMap<_, _> = Url::parse(&authorize_url) + .unwrap() + .query_pairs() + .into_owned() + .collect(); + + assert_eq!(hash_query.get("client_id").unwrap(), "this-is-my-client-id"); + assert_eq!(hash_query.get("response_type").unwrap(), "code"); + assert_eq!(hash_query.get("redirect_uri").unwrap(), "localhost"); + assert_eq!(hash_query.get("scope").unwrap(), "playlist-read-private"); + assert_eq!(hash_query.get("state").unwrap(), "fdsafdsfa"); +} + +#[maybe_async] +#[maybe_async_test] +async fn test_read_token_cache() { + let now: DateTime = Utc::now(); + let scope = scopes!("playlist-read-private", "playlist-read-collaborative"); + + let tok = TokenBuilder::default() + .access_token("test-access_token") + .expires_in(Duration::seconds(3600)) + .expires_at(now) + .scope(scope.clone()) + .refresh_token("...") + .build() + .unwrap(); + + let predefined_spotify = SpotifyBuilder::default() + .token(tok.clone()) + .cache_path(PathBuf::from(".test_read_token_cache.json")) + .build() + .unwrap(); + + // write token data to cache_path + predefined_spotify.write_token_cache().unwrap(); + assert!(predefined_spotify.cache_path.exists()); + + let oauth_scope = scopes!("playlist-read-private"); + let oauth = OAuthBuilder::default() + .state("fdasfasfdasd") + .redirect_uri("http://localhost:8000") + .scope(oauth_scope) + .build() + .unwrap(); + + let mut spotify = SpotifyBuilder::default() + .oauth(oauth) + .cache_path(PathBuf::from(".test_read_token_cache.json")) + .build() + .unwrap(); + // read token from cache file + let tok_from_file = spotify.read_token_cache().await.unwrap(); + assert_eq!(tok_from_file.scope, scope); + assert_eq!(tok_from_file.refresh_token.unwrap(), "..."); + assert_eq!(tok_from_file.expires_in, Duration::seconds(3600)); + assert_eq!(tok_from_file.expires_at.unwrap(), now); + + // delete cache file in the end + fs::remove_file(&spotify.cache_path).unwrap(); +} + +#[test] +fn test_write_token() { + let now: DateTime = Utc::now(); + let scope = scopes!("playlist-read-private", "playlist-read-collaborative"); + + let tok = TokenBuilder::default() + .access_token("test-access_token") + .expires_in(Duration::seconds(3600)) + .expires_at(now) + .scope(scope.clone()) + .refresh_token("...") + .build() + .unwrap(); + + let spotify = SpotifyBuilder::default() + .token(tok.clone()) + .cache_path(PathBuf::from(".test_write_token_cache.json")) + .build() + .unwrap(); + + let tok_str = serde_json::to_string(&tok).unwrap(); + spotify.write_token_cache().unwrap(); + + let mut file = fs::File::open(&spotify.cache_path).unwrap(); + let mut tok_str_file = String::new(); + file.read_to_string(&mut tok_str_file).unwrap(); + + assert_eq!(tok_str, tok_str_file); + let tok_from_file: Token = serde_json::from_str(&tok_str_file).unwrap(); + assert_eq!(tok_from_file.scope, scope); + assert_eq!(tok_from_file.expires_in, Duration::seconds(3600)); + assert_eq!(tok_from_file.expires_at.unwrap(), now); + + // delete cache file in the end + fs::remove_file(&spotify.cache_path).unwrap(); +} + +#[test] +fn test_token_is_expired() { + let scope = scopes!("playlist-read-private", "playlist-read-collaborative"); + + let tok = TokenBuilder::default() + .access_token("test-access_token") + .expires_in(Duration::seconds(1)) + .expires_at(Utc::now()) + .scope(scope) + .refresh_token("...") + .build() + .unwrap(); + assert!(!tok.is_expired()); + sleep(std::time::Duration::from_secs(2)); + assert!(tok.is_expired()); +} + +#[test] +fn test_parse_response_code() { + let spotify = SpotifyBuilder::default().build().unwrap(); + + let url = "http://localhost:8888/callback"; + let code = spotify.parse_response_code(url); + assert_eq!(code, None); + + let url = "http://localhost:8888/callback?code=AQD0yXvFEOvw"; + let code = spotify.parse_response_code(url); + assert_eq!(code, Some("AQD0yXvFEOvw".to_string())); + + let url = "http://localhost:8888/callback?code=AQD0yXvFEOvw&state=sN#_=_"; + let code = spotify.parse_response_code(url); + assert_eq!(code, Some("AQD0yXvFEOvw".to_string())); + + let url = "http://localhost:8888/callback?state=sN&code=AQD0yXvFEOvw#_=_"; + let code = spotify.parse_response_code(url); + assert_eq!(code, Some("AQD0yXvFEOvw".to_string())); +} diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 28b68ba5..611a18e6 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -24,6 +24,7 @@ use rspotify::{ model::{ Country, Id, Market, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, }, + scopes, }; use chrono::prelude::*; @@ -53,16 +54,25 @@ pub async fn oauth_client() -> Spotify { ) }); - let scope = "user-read-email user-read-private user-top-read \ - user-read-recently-played user-follow-read user-library-read \ - user-read-currently-playing user-read-playback-state \ - user-read-playback-position playlist-read-collaborative \ - playlist-read-private user-follow-modify user-library-modify \ - user-modify-playback-state playlist-modify-public \ - playlist-modify-private ugc-image-upload" - .split_whitespace() - .map(|x| x.to_owned()) - .collect(); + let scope = scopes!( + "user-read-email", + "user-read-private", + "user-top-read", + "user-read-recently-played", + "user-follow-read", + "user-library-read", + "user-read-currently-playing", + "user-read-playback-state", + "user-read-playback-position", + "playlist-read-collaborative", + "playlist-read-private", + "user-follow-modify", + "user-library-modify", + "user-modify-playback-state", + "playlist-modify-public", + "playlist-modify-private", + "ugc-image-upload" + ); // Using every possible scope let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap();