From e284894d40e206adf8089482e340d7a9d0046d79 Mon Sep 17 00:00:00 2001 From: Paul Woolcock Date: Wed, 29 Aug 2018 16:23:50 -0400 Subject: [PATCH] feat(scopes): Implement granular OAuth scopes BREAKING CHANGE: Applications that use the `Scopes` data structure will have minor changes to make Closes #44 --- examples/register.rs | 2 +- src/apps.rs | 30 +- src/registration.rs | 10 +- src/scopes.rs | 668 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 641 insertions(+), 69 deletions(-) diff --git a/examples/register.rs b/examples/register.rs index f375921..7b581ee 100644 --- a/examples/register.rs +++ b/examples/register.rs @@ -31,7 +31,7 @@ pub fn register() -> Result> { let website = read_line("Please enter your mastodon instance url:")?; let registration = Registration::new(website.trim()) .client_name("elefren-examples") - .scopes(Scopes::All) + .scopes(Scopes::all()) .website("https://github.com/pwoolcoc/elefren") .build()?; let url = registration.authorize_url()?; diff --git a/src/apps.rs b/src/apps.rs index c1bd06e..f9e1585 100644 --- a/src/apps.rs +++ b/src/apps.rs @@ -2,8 +2,8 @@ use std::borrow::Cow; use try_from::TryInto; -use scopes::Scopes; use errors::{Error, Result}; +use scopes::Scopes; /// Represents an application that can be registered with a mastodon instance #[derive(Clone, Debug, Default, Serialize, PartialEq)] @@ -37,19 +37,19 @@ impl App { /// ``` /// # extern crate elefren; /// # use elefren::Error; - /// use elefren::apps::{App, Scopes}; + /// use elefren::{apps::App, scopes::Scopes}; /// /// # fn main() -> Result<(), Error> { /// let mut builder = App::builder(); /// builder.client_name("elefren-test"); /// let app = builder.build()?; /// let scopes = app.scopes(); - /// assert_eq!(scopes, Scopes::Read); + /// assert_eq!(scopes, &Scopes::read_all()); /// # Ok(()) /// # } /// ``` - pub fn scopes(&self) -> Scopes { - self.scopes + pub fn scopes(&self) -> &Scopes { + &self.scopes } } @@ -98,7 +98,7 @@ impl<'a> AppBuilder<'a> { /// Permission scope of the application. /// - /// IF none is specified, the default is Scopes::Read + /// IF none is specified, the default is Scopes::read_all() pub fn scopes(&mut self, scopes: Scopes) -> &mut Self { self.scopes = Some(scopes); self @@ -123,7 +123,7 @@ impl<'a> AppBuilder<'a> { .redirect_uris .unwrap_or_else(|| "urn:ietf:wg:oauth:2.0:oob".into()) .into(), - scopes: self.scopes.unwrap_or_else(|| Scopes::Read), + scopes: self.scopes.unwrap_or_else(|| Scopes::read_all()), website: self.website.map(|s| s.into()), }) } @@ -158,9 +158,9 @@ mod tests { #[test] fn test_app_scopes() { let mut builder = App::builder(); - builder.client_name("test").scopes(Scopes::All); + builder.client_name("test").scopes(Scopes::all()); let app = builder.build().expect("Couldn't build App"); - assert_eq!(app.scopes(), Scopes::All); + assert_eq!(app.scopes(), &Scopes::all()); } #[test] @@ -168,7 +168,7 @@ mod tests { let mut builder = AppBuilder::new(); builder.client_name("foo-test"); builder.redirect_uris("http://example.com"); - builder.scopes(Scopes::ReadWrite); + builder.scopes(Scopes::read_all() | Scopes::write_all()); builder.website("https://example.com"); let app = builder.build().expect("Couldn't build App"); assert_eq!( @@ -176,7 +176,7 @@ mod tests { App { client_name: "foo-test".to_string(), redirect_uris: "http://example.com".to_string(), - scopes: Scopes::ReadWrite, + scopes: Scopes::read_all() | Scopes::write_all(), website: Some("https://example.com".to_string()), } ); @@ -195,7 +195,7 @@ mod tests { builder .website("https://example.com") .redirect_uris("https://example.com") - .scopes(Scopes::All); + .scopes(Scopes::all()); builder.build().expect("no client-name"); } @@ -204,7 +204,7 @@ mod tests { let app = App { client_name: "foo-test".to_string(), redirect_uris: "http://example.com".to_string(), - scopes: Scopes::All, + scopes: Scopes::all(), website: None, }; let expected = app.clone(); @@ -218,11 +218,11 @@ mod tests { builder .client_name("foo-test") .redirect_uris("http://example.com") - .scopes(Scopes::All); + .scopes(Scopes::all()); let expected = App { client_name: "foo-test".to_string(), redirect_uris: "http://example.com".to_string(), - scopes: Scopes::All, + scopes: Scopes::all(), website: None, }; let result = builder diff --git a/src/registration.rs b/src/registration.rs index 2a5f8d7..d09c180 100644 --- a/src/registration.rs +++ b/src/registration.rs @@ -4,8 +4,8 @@ use reqwest::{Client, RequestBuilder, Response}; use try_from::TryInto; use apps::{App, AppBuilder}; -use scopes::Scopes; use http_send::{HttpSend, HttpSender}; +use scopes::Scopes; use Data; use Error; use Mastodon; @@ -136,7 +136,7 @@ impl<'a, H: HttpSend> Registration<'a, H> { client_id: oauth.client_id, client_secret: oauth.client_secret, redirect: oauth.redirect_uri, - scopes: app.scopes(), + scopes: app.scopes().clone(), http_sender: self.http_sender.clone(), }) } @@ -171,7 +171,7 @@ impl<'a, H: HttpSend> Registration<'a, H> { client_id: oauth.client_id, client_secret: oauth.client_secret, redirect: oauth.redirect_uri, - scopes: app.scopes(), + scopes: app.scopes().clone(), http_sender: self.http_sender.clone(), }) } @@ -286,10 +286,10 @@ mod tests { #[test] fn test_set_scopes() { let mut r = Registration::new("https://example.com"); - r.scopes(Scopes::All); + r.scopes(Scopes::all()); assert_eq!(r.base, "https://example.com".to_string()); - assert_eq!(&mut r.app_builder, AppBuilder::new().scopes(Scopes::All)); + assert_eq!(&mut r.app_builder, AppBuilder::new().scopes(Scopes::all())); } #[test] diff --git a/src/scopes.rs b/src/scopes.rs index 282d1f9..07e9828 100644 --- a/src/scopes.rs +++ b/src/scopes.rs @@ -1,82 +1,619 @@ -use std::fmt; +use std::{ + cmp::{Ordering, PartialEq, PartialOrd}, + collections::HashSet, + fmt, + ops::BitOr, +}; + +use serde::ser::{Serialize, Serializer}; + +/// Represents a set of OAuth scopes +/// +/// # Example +/// +/// ```rust +/// use elefren::prelude::*; +/// +/// let read = Scopes::read_all(); +/// let write = Scopes::write_all(); +/// let follow = Scopes::follow(); +/// let all = read | write | follow; +/// ``` +#[derive(Clone)] +pub struct Scopes { + scopes: HashSet, +} + +impl Serialize for Scopes { + fn serialize(&self, serializer: S) -> ::std::result::Result + where + S: Serializer, + { + let repr = format!("{}", self); + serializer.serialize_str(&repr) + } +} + +impl Scopes { + /// Represents all available oauth scopes: "read write follow push" + /// + /// ``` + /// # extern crate elefren; + /// # use std::error::Error; + /// use elefren::scopes::Scopes; + /// + /// # fn main() -> Result<(), Box> { + /// let scope = Scopes::all(); + /// assert_eq!(&format!("{}", scope), "read write follow push"); + /// # Ok(()) + /// # } + /// ``` + pub fn all() -> Scopes { + Scopes::read_all() | Scopes::write_all() | Scopes::follow() | Scopes::push() + } + + /// Represents the full "read" scope + /// + /// ``` + /// # extern crate elefren; + /// # use std::error::Error; + /// use elefren::scopes::Scopes; + /// + /// # fn main() -> Result<(), Box> { + /// let scope = Scopes::read_all(); + /// assert_eq!(&format!("{}", scope), "read"); + /// # Ok(()) + /// # } + /// ``` + pub fn read_all() -> Scopes { + Scopes::_read(None) + } + + /// Represents a specific "read:___" scope + /// + /// ``` + /// # extern crate elefren; + /// # use std::error::Error; + /// use elefren::scopes::{Read, Scopes}; + /// + /// # fn main() -> Result<(), Box> { + /// let scope = Scopes::read(Read::Accounts); + /// assert_eq!(&format!("{}", scope), "read:accounts"); + /// # Ok(()) + /// # } + /// ``` + pub fn read(subscope: Read) -> Scopes { + Scopes::_read(Some(subscope)) + } + + /// Represents the full "write" scope + /// + /// ``` + /// # extern crate elefren; + /// # use std::error::Error; + /// use elefren::scopes::Scopes; + /// + /// # fn main() -> Result<(), Box> { + /// let scope = Scopes::write_all(); + /// assert_eq!(&format!("{}", scope), "write"); + /// # Ok(()) + /// # } + /// ``` + pub fn write_all() -> Scopes { + Scopes::_write(None) + } + + /// Represents a specific "write:___" scope + /// + /// ``` + /// # extern crate elefren; + /// # use std::error::Error; + /// use elefren::scopes::{Scopes, Write}; + /// + /// # fn main() -> Result<(), Box> { + /// let scope = Scopes::write(Write::Accounts); + /// assert_eq!(&format!("{}", scope), "write:accounts"); + /// # Ok(()) + /// # } + /// ``` + pub fn write(subscope: Write) -> Scopes { + Scopes::_write(Some(subscope)) + } + + /// Represents the "follow" scope + /// + /// ``` + /// # extern crate elefren; + /// # use std::error::Error; + /// use elefren::scopes::Scopes; + /// + /// # fn main() -> Result<(), Box> { + /// let scope = Scopes::follow(); + /// assert_eq!(&format!("{}", scope), "follow"); + /// # Ok(()) + /// # } + /// ``` + pub fn follow() -> Scopes { + Scopes::new(Scope::Follow) + } + + /// Represents the full "push" scope + /// + /// ``` + /// # extern crate elefren; + /// # use std::error::Error; + /// use elefren::scopes::Scopes; + /// + /// # fn main() -> Result<(), Box> { + /// let scope = Scopes::push(); + /// assert_eq!(&format!("{}", scope), "push"); + /// # Ok(()) + /// # } + /// ``` + pub fn push() -> Scopes { + Scopes::new(Scope::Push) + } + + /// Combines 2 scopes together + /// + /// # Example + /// + /// ```rust + /// use elefren::prelude::*; + /// + /// let read = Scopes::read_all(); + /// let write = Scopes::write_all(); + /// let read_write = read.and(write); + /// ``` + pub fn and(self, other: Scopes) -> Scopes { + let newset: HashSet<_> = self + .scopes + .union(&other.scopes) + .into_iter() + .map(|s| *s) + .collect(); + Scopes { + scopes: newset, + } + } + + fn _write(subscope: Option) -> Scopes { + Scopes::new(Scope::Write(subscope)) + } + + fn _read(subscope: Option) -> Scopes { + Scopes::new(Scope::Read(subscope)) + } + + fn new(scope: Scope) -> Scopes { + let mut set = HashSet::new(); + set.insert(scope); + Scopes { + scopes: set, + } + } +} + +impl BitOr for Scopes { + type Output = Scopes; + + fn bitor(self, other: Scopes) -> Self::Output { + self.and(other) + } +} + +impl PartialEq for Scopes { + fn eq(&self, other: &Scopes) -> bool { + self.scopes + .symmetric_difference(&other.scopes) + .next() + .is_none() + } +} + +impl Default for Scopes { + fn default() -> Scopes { + Scopes::read_all() + } +} + +impl fmt::Debug for Scopes { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "[")?; + for scope in &self.scopes { + write!(f, "{:?}", &scope)?; + } + Ok(write!(f, "]")?) + } +} + +impl fmt::Display for Scopes { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut start = true; + let scopes = { + let mut scopes = self.scopes.iter().collect::>(); + scopes.sort(); + scopes + }; + for scope in &scopes { + if !start { + write!(f, " ")?; + } else { + start = false; + } + write!(f, "{}", &scope)?; + } + Ok(()) + } +} /// Permission scope of the application. /// [Details on what each permission provides][1] /// [1]: https://github.com/tootsuite/documentation/blob/master/Using-the-API/OAuth-details.md) -#[derive(Debug, Clone, Copy, PartialEq, Hash, Serialize)] -pub enum Scopes { - /// All Permissions, equivalent to `read write follow` - #[serde(rename = "read write follow")] - All, - /// Only permission to add and remove followers. - #[serde(rename = "follow")] - Follow, +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] +enum Scope { /// Read only permissions. #[serde(rename = "read")] - Read, - /// Read & Follow permissions. - #[serde(rename = "read follow")] - ReadFollow, - /// Read & Write permissions. - #[serde(rename = "read write")] - ReadWrite, + Read(Option), /// Write only permissions. #[serde(rename = "write")] - Write, - /// Write & Follow permissions. - #[serde(rename = "write follow")] - WriteFollow, + Write(Option), + /// Only permission to add and remove followers. + #[serde(rename = "follow")] + Follow, + /// Push permissions + #[serde(rename = "push")] + Push, } -impl fmt::Display for Scopes { +impl PartialOrd for Scope { + fn partial_cmp(&self, other: &Scope) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Scope { + fn cmp(&self, other: &Scope) -> Ordering { + match (*self, *other) { + (Scope::Read(None), Scope::Read(None)) => Ordering::Equal, + (Scope::Read(None), Scope::Read(Some(..))) => Ordering::Less, + (Scope::Read(Some(..)), Scope::Read(None)) => Ordering::Greater, + (Scope::Read(Some(ref a)), Scope::Read(Some(ref b))) => a.cmp(b), + + (Scope::Write(None), Scope::Write(None)) => Ordering::Equal, + (Scope::Write(None), Scope::Write(Some(..))) => Ordering::Less, + (Scope::Write(Some(..)), Scope::Write(None)) => Ordering::Greater, + (Scope::Write(Some(ref a)), Scope::Write(Some(ref b))) => a.cmp(b), + + (Scope::Read(..), Scope::Write(..)) => Ordering::Less, + (Scope::Read(..), Scope::Follow) => Ordering::Less, + (Scope::Read(..), Scope::Push) => Ordering::Less, + + (Scope::Write(..), Scope::Read(..)) => Ordering::Greater, + (Scope::Write(..), Scope::Follow) => Ordering::Less, + (Scope::Write(..), Scope::Push) => Ordering::Less, + + (Scope::Follow, Scope::Read(..)) => Ordering::Greater, + (Scope::Follow, Scope::Write(..)) => Ordering::Greater, + (Scope::Follow, Scope::Follow) => Ordering::Equal, + (Scope::Follow, Scope::Push) => Ordering::Less, + + (Scope::Push, Scope::Push) => Ordering::Equal, + (Scope::Push, _) => Ordering::Greater, + } + } +} + +impl fmt::Display for Scope { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Scope::*; + let s = match *self { + Read(Some(ref r)) => return fmt::Display::fmt(r, f), + Read(None) => "read", + Write(Some(ref w)) => return fmt::Display::fmt(w, f), + Write(None) => "write", + Follow => "follow", + Push => "push", + }; + write!(f, "{}", s) + } +} + +impl Default for Scope { + fn default() -> Self { + Scope::Read(None) + } +} + +/// Represents the granular "read:___" oauth scopes +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] +pub enum Read { + /// Accounts + #[serde(rename = "accounts")] + Accounts, + /// Blocks + #[serde(rename = "blocks")] + Blocks, + /// Favourites + #[serde(rename = "favourites")] + Favourites, + /// Filters + #[serde(rename = "filters")] + Filters, + /// Follows + #[serde(rename = "follows")] + Follows, + /// Lists + #[serde(rename = "lists")] + Lists, + /// Mutes + #[serde(rename = "mutes")] + Mutes, + /// Notifications + #[serde(rename = "notifications")] + Notifications, + /// Reports + #[serde(rename = "reports")] + Reports, + /// Search + #[serde(rename = "search")] + Search, + /// Statuses + #[serde(rename = "statuses")] + Statuses, +} + +impl PartialOrd for Read { + fn partial_cmp(&self, other: &Read) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Read { + fn cmp(&self, other: &Read) -> Ordering { + let a = format!("{:?}", self); + let b = format!("{:?}", other); + a.cmp(&b) + } +} + +impl fmt::Display for Read { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Scopes::*; write!( f, - "{}", + "read:{}", match *self { - All => "read%20write%20follow", - Follow => "follow", - Read => "read", - ReadFollow => "read%20follow", - ReadWrite => "read%20write", - Write => "write", - WriteFollow => "write%20follow", + Read::Accounts => "accounts", + Read::Blocks => "blocks", + Read::Favourites => "favourites", + Read::Filters => "filters", + Read::Follows => "follows", + Read::Lists => "lists", + Read::Mutes => "mutes", + Read::Notifications => "notifications", + Read::Reports => "reports", + Read::Search => "search", + Read::Statuses => "statuses", } ) } } -impl Default for Scopes { - fn default() -> Self { - Scopes::Read +/// Represents the granular "write:___" oauth scopes +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] +pub enum Write { + /// Accounts + #[serde(rename = "accounts")] + Accounts, + /// Blocks + #[serde(rename = "blocks")] + Blocks, + /// Favourites + #[serde(rename = "favourites")] + Favourites, + /// Filters + #[serde(rename = "filters")] + Filters, + /// Follows + #[serde(rename = "follows")] + Follows, + /// Lists + #[serde(rename = "lists")] + Lists, + /// Media + #[serde(rename = "media")] + Media, + /// Mutes + #[serde(rename = "mutes")] + Mutes, + /// Notifications + #[serde(rename = "notifications")] + Notifications, + /// Reports + #[serde(rename = "reports")] + Reports, + /// Statuses + #[serde(rename = "statuses")] + Statuses, +} + +impl PartialOrd for Write { + fn partial_cmp(&self, other: &Write) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Write { + fn cmp(&self, other: &Write) -> Ordering { + let a = format!("{:?}", self); + let b = format!("{:?}", other); + a.cmp(&b) + } +} + +impl fmt::Display for Write { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "write:{}", + match *self { + Write::Accounts => "accounts", + Write::Blocks => "blocks", + Write::Favourites => "favourites", + Write::Filters => "filters", + Write::Follows => "follows", + Write::Lists => "lists", + Write::Media => "media", + Write::Mutes => "mutes", + Write::Notifications => "notifications", + Write::Reports => "reports", + Write::Statuses => "statuses", + } + ) } } #[cfg(test)] mod tests { use super::*; + use serde_json; #[test] - fn test_scopes_display() { + fn test_write_cmp() { + let tests = [ + (Write::Accounts, Write::Blocks), + (Write::Blocks, Write::Favourites), + (Write::Favourites, Write::Filters), + (Write::Filters, Write::Follows), + (Write::Follows, Write::Lists), + (Write::Lists, Write::Media), + (Write::Media, Write::Mutes), + (Write::Mutes, Write::Notifications), + (Write::Notifications, Write::Reports), + (Write::Reports, Write::Statuses), + ]; + + for (a, b) in &tests { + assert!(a < b); + assert!(b > a); + } + } + + #[test] + fn test_read_cmp() { + let tests = [ + (Read::Accounts, Read::Blocks), + (Read::Blocks, Read::Favourites), + (Read::Favourites, Read::Filters), + (Read::Filters, Read::Follows), + (Read::Follows, Read::Lists), + (Read::Lists, Read::Mutes), + (Read::Mutes, Read::Notifications), + (Read::Notifications, Read::Reports), + (Read::Reports, Read::Search), + (Read::Search, Read::Statuses), + ]; + for (a, b) in &tests { + assert!(a < b); + assert!(b > a); + } + } + + #[test] + fn test_scope_cmp() { + let tests = [ + (Scope::Read(None), Scope::Read(Some(Read::Accounts))), + (Scope::Read(None), Scope::Read(Some(Read::Blocks))), + (Scope::Read(None), Scope::Read(Some(Read::Favourites))), + (Scope::Read(None), Scope::Read(Some(Read::Filters))), + (Scope::Read(None), Scope::Read(Some(Read::Follows))), + (Scope::Read(None), Scope::Read(Some(Read::Lists))), + (Scope::Read(None), Scope::Read(Some(Read::Mutes))), + (Scope::Read(None), Scope::Read(Some(Read::Notifications))), + (Scope::Read(None), Scope::Read(Some(Read::Reports))), + (Scope::Read(None), Scope::Read(Some(Read::Search))), + (Scope::Read(None), Scope::Read(Some(Read::Statuses))), + (Scope::Read(Some(Read::Statuses)), Scope::Write(None)), + (Scope::Read(Some(Read::Mutes)), Scope::Follow), + (Scope::Read(None), Scope::Push), + (Scope::Write(None), Scope::Write(Some(Write::Accounts))), + (Scope::Write(None), Scope::Write(Some(Write::Blocks))), + (Scope::Write(None), Scope::Write(Some(Write::Favourites))), + (Scope::Write(None), Scope::Write(Some(Write::Filters))), + (Scope::Write(None), Scope::Write(Some(Write::Follows))), + (Scope::Write(None), Scope::Write(Some(Write::Lists))), + (Scope::Write(None), Scope::Write(Some(Write::Media))), + (Scope::Write(None), Scope::Write(Some(Write::Mutes))), + (Scope::Write(None), Scope::Write(Some(Write::Notifications))), + (Scope::Write(None), Scope::Write(Some(Write::Reports))), + (Scope::Write(None), Scope::Write(Some(Write::Statuses))), + (Scope::Write(Some(Write::Statuses)), Scope::Follow), + (Scope::Write(Some(Write::Follows)), Scope::Push), + ]; + + for (a, b) in &tests { + assert!(a < b); + } + } + + #[test] + fn test_scope_display() { let values = [ - Scopes::All, - Scopes::Follow, - Scopes::Read, - Scopes::ReadFollow, - Scopes::ReadWrite, - Scopes::Write, - Scopes::WriteFollow, + Scope::Read(None), + Scope::Read(Some(Read::Accounts)), + Scope::Read(Some(Read::Blocks)), + Scope::Read(Some(Read::Favourites)), + Scope::Read(Some(Read::Filters)), + Scope::Read(Some(Read::Follows)), + Scope::Read(Some(Read::Lists)), + Scope::Read(Some(Read::Mutes)), + Scope::Read(Some(Read::Notifications)), + Scope::Read(Some(Read::Reports)), + Scope::Read(Some(Read::Search)), + Scope::Read(Some(Read::Statuses)), + Scope::Write(None), + Scope::Write(Some(Write::Accounts)), + Scope::Write(Some(Write::Blocks)), + Scope::Write(Some(Write::Favourites)), + Scope::Write(Some(Write::Filters)), + Scope::Write(Some(Write::Follows)), + Scope::Write(Some(Write::Lists)), + Scope::Write(Some(Write::Media)), + Scope::Write(Some(Write::Mutes)), + Scope::Write(Some(Write::Notifications)), + Scope::Write(Some(Write::Reports)), + Scope::Write(Some(Write::Statuses)), + Scope::Follow, + Scope::Push, ]; let expecteds = [ - "read%20write%20follow".to_string(), - "follow".to_string(), "read".to_string(), - "read%20follow".to_string(), - "read%20write".to_string(), + "read:accounts".to_string(), + "read:blocks".to_string(), + "read:favourites".to_string(), + "read:filters".to_string(), + "read:follows".to_string(), + "read:lists".to_string(), + "read:mutes".to_string(), + "read:notifications".to_string(), + "read:reports".to_string(), + "read:search".to_string(), + "read:statuses".to_string(), "write".to_string(), - "write%20follow".to_string(), + "write:accounts".to_string(), + "write:blocks".to_string(), + "write:favourites".to_string(), + "write:filters".to_string(), + "write:follows".to_string(), + "write:lists".to_string(), + "write:media".to_string(), + "write:mutes".to_string(), + "write:notifications".to_string(), + "write:reports".to_string(), + "write:statuses".to_string(), + "follow".to_string(), + "push".to_string(), ]; let tests = values.into_iter().zip(expecteds.into_iter()); @@ -89,7 +626,42 @@ mod tests { #[test] fn test_scopes_default() { - let default: Scopes = Default::default(); - assert_eq!(default, Scopes::Read); + let default: Scope = Default::default(); + assert_eq!(default, Scope::Read(None)); + } + + #[test] + fn test_scopes_display() { + let tests = [ + ( + Scopes::read(Read::Accounts) | Scopes::follow(), + "read:accounts follow", + ), + ( + Scopes::read(Read::Follows) | Scopes::read(Read::Accounts) | Scopes::write_all(), + "read:accounts read:follows write", + ), + ]; + + for (a, b) in &tests { + assert_eq!(&format!("{}", a), b); + } + } + + #[test] + fn test_scopes_serialize() { + let tests = [ + ( + Scopes::read_all() | Scopes::write(Write::Notifications) | Scopes::follow(), + "read write:notifications follow", + ), + (Scopes::follow() | Scopes::push(), "follow push"), + ]; + + for (a, b) in &tests { + let ser = serde_json::to_string(&a).expect("Couldn't serialize Scopes"); + let expected = format!("\"{}\"", b); + assert_eq!(&ser, &expected); + } } }