diff options
| author | Mishio595 <[email protected]> | 2018-08-01 15:38:12 -0600 |
|---|---|---|
| committer | Mishio595 <[email protected]> | 2018-08-01 15:38:12 -0600 |
| commit | c5fb7b4b331ef5a66179539b065913078e55b668 (patch) | |
| tree | 99bc28270eaad9acf3da3871e72ba67dac5b87eb /src | |
| parent | Merge branch 'asref_messageid_for_message' (diff) | |
| parent | Don't delay Ready with cache enabled (diff) | |
| download | serenity-c5fb7b4b331ef5a66179539b065913078e55b668.tar.xz serenity-c5fb7b4b331ef5a66179539b065913078e55b668.zip | |
Merge branch 'upstream'
Diffstat (limited to 'src')
| -rw-r--r-- | src/builder/create_embed.rs | 89 | ||||
| -rw-r--r-- | src/cache/mod.rs | 172 | ||||
| -rw-r--r-- | src/client/bridge/gateway/shard_messenger.rs | 16 | ||||
| -rw-r--r-- | src/client/context.rs | 21 | ||||
| -rw-r--r-- | src/client/dispatch.rs | 57 | ||||
| -rw-r--r-- | src/framework/standard/args.rs | 433 | ||||
| -rw-r--r-- | src/framework/standard/help_commands.rs | 4 | ||||
| -rw-r--r-- | src/framework/standard/mod.rs | 30 | ||||
| -rw-r--r-- | src/http/mod.rs | 18 | ||||
| -rw-r--r-- | src/lib.rs | 4 | ||||
| -rw-r--r-- | src/model/channel/channel_id.rs | 6 | ||||
| -rw-r--r-- | src/model/channel/mod.rs | 90 | ||||
| -rw-r--r-- | src/model/gateway.rs | 52 | ||||
| -rw-r--r-- | src/model/guild/guild_id.rs | 12 | ||||
| -rw-r--r-- | src/model/guild/mod.rs | 106 | ||||
| -rw-r--r-- | src/model/misc.rs | 80 | ||||
| -rw-r--r-- | src/model/user.rs | 73 | ||||
| -rw-r--r-- | src/utils/colour.rs | 52 | ||||
| -rw-r--r-- | src/utils/message_builder.rs | 78 | ||||
| -rw-r--r-- | src/utils/mod.rs | 51 |
20 files changed, 1362 insertions, 82 deletions
diff --git a/src/builder/create_embed.rs b/src/builder/create_embed.rs index 7604386..35172e1 100644 --- a/src/builder/create_embed.rs +++ b/src/builder/create_embed.rs @@ -515,3 +515,92 @@ impl<'a, Tz: TimeZone> From<&'a DateTime<Tz>> for Timestamp } } } + +#[cfg(test)] +mod test { + use model::channel::{Embed, EmbedField, EmbedFooter, EmbedImage, EmbedVideo}; + use serde_json::Value; + use super::CreateEmbed; + use utils::{self, Colour}; + + #[test] + fn test_from_embed() { + let embed = Embed { + author: None, + colour: Colour::new(0xFF0011), + description: Some("This is a test description".to_string()), + fields: vec![ + EmbedField { + inline: false, + name: "a".to_string(), + value: "b".to_string(), + }, + EmbedField { + inline: true, + name: "c".to_string(), + value: "z".to_string(), + }, + ], + footer: Some(EmbedFooter { + icon_url: Some("https://i.imgur.com/XfWpfCV.gif".to_string()), + proxy_icon_url: None, + text: "This is a hakase footer".to_string(), + }), + image: Some(EmbedImage { + height: 213, + proxy_url: "a".to_string(), + url: "https://i.imgur.com/XfWpfCV.gif".to_string(), + width: 224, + }), + kind: "rich".to_string(), + provider: None, + thumbnail: None, + timestamp: None, + title: Some("hakase".to_string()), + url: Some("https://i.imgur.com/XfWpfCV.gif".to_string()), + video: Some(EmbedVideo { + height: 213, + url: "https://i.imgur.com/XfWpfCV.mp4".to_string(), + width: 224, + }), + }; + + let builder = CreateEmbed::from(embed) + .colour(0xFF0011) + .description("This is a hakase description") + .image("https://i.imgur.com/XfWpfCV.gif") + .title("still a hakase") + .url("https://i.imgur.com/XfWpfCV.gif"); + + let built = Value::Object(utils::vecmap_to_json_map(builder.0)); + + let obj = json!({ + "color": 0xFF0011, + "description": "This is a hakase description", + "title": "still a hakase", + "type": "rich", + "url": "https://i.imgur.com/XfWpfCV.gif", + "fields": [ + { + "inline": false, + "name": "a", + "value": "b", + }, + { + "inline": true, + "name": "c", + "value": "z", + }, + ], + "image": { + "url": "https://i.imgur.com/XfWpfCV.gif", + }, + "footer": { + "text": "This is a hakase footer", + "icon_url": "https://i.imgur.com/XfWpfCV.gif", + } + }); + + assert_eq!(built, obj); + } +} diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 913fe21..0a09623 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -802,3 +802,175 @@ impl Default for Cache { } } } + +#[cfg(test)] +mod test { + use chrono::DateTime; + use serde_json::{Number, Value}; + use std::{ + collections::HashMap, + sync::Arc, + }; + use { + cache::{Cache, CacheUpdate, Settings}, + model::prelude::*, + prelude::RwLock, + }; + + #[test] + fn test_cache_messages() { + let mut settings = Settings::new(); + settings.max_messages(2); + let mut cache = Cache::new_with_settings(settings); + + // Test inserting one message into a channel's message cache. + let datetime = DateTime::parse_from_str( + "1983 Apr 13 12:09:14.274 +0000", + "%Y %b %d %H:%M:%S%.3f %z", + ).unwrap(); + let mut event = MessageCreateEvent { + message: Message { + id: MessageId(3), + attachments: vec![], + author: User { + id: UserId(2), + avatar: None, + bot: false, + discriminator: 1, + name: "user 1".to_owned(), + }, + channel_id: ChannelId(2), + guild_id: Some(GuildId(1)), + content: String::new(), + edited_timestamp: None, + embeds: vec![], + kind: MessageType::Regular, + member: None, + mention_everyone: false, + mention_roles: vec![], + mentions: vec![], + nonce: Value::Number(Number::from(1)), + pinned: false, + reactions: vec![], + timestamp: datetime.clone(), + tts: false, + webhook_id: None, + }, + }; + // Check that the channel cache doesn't exist. + assert!(!cache.messages.contains_key(&event.message.channel_id)); + // Add first message, none because message ID 2 doesn't already exist. + assert!(event.update(&mut cache).is_none()); + // None, it only returns the oldest message if the cache was already full. + assert!(event.update(&mut cache).is_none()); + // Assert there's only 1 message in the channel's message cache. + assert_eq!(cache.messages.get(&event.message.channel_id).unwrap().len(), 1); + + // Add a second message, assert that channel message cache length is 2. + event.message.id = MessageId(4); + assert!(event.update(&mut cache).is_none()); + assert_eq!(cache.messages.get(&event.message.channel_id).unwrap().len(), 2); + + // Add a third message, the first should now be removed. + event.message.id = MessageId(5); + assert!(event.update(&mut cache).is_some()); + + { + let channel = cache.messages.get(&event.message.channel_id).unwrap(); + + assert_eq!(channel.len(), 2); + // Check that the first message is now removed. + assert!(!channel.contains_key(&MessageId(3))); + } + + let guild_channel = GuildChannel { + id: event.message.channel_id, + bitrate: None, + category_id: None, + guild_id: event.message.guild_id.unwrap(), + kind: ChannelType::Text, + last_message_id: None, + last_pin_timestamp: None, + name: String::new(), + permission_overwrites: vec![], + position: 0, + topic: None, + user_limit: None, + nsfw: false, + }; + + // Add a channel delete event to the cache, the cached messages for that + // channel should now be gone. + let mut delete = ChannelDeleteEvent { + channel: Channel::Guild(Arc::new(RwLock::new(guild_channel.clone()))), + }; + assert!(cache.update(&mut delete).is_none()); + assert!(!cache.messages.contains_key(&delete.channel.id())); + + // Test deletion of a guild channel's message cache when a GuildDeleteEvent + // is received. + let mut guild_create = { + let mut channels = HashMap::new(); + channels.insert(ChannelId(2), Arc::new(RwLock::new(guild_channel.clone()))); + + GuildCreateEvent { + guild: Guild { + id: GuildId(1), + afk_channel_id: None, + afk_timeout: 0, + application_id: None, + default_message_notifications: DefaultMessageNotificationLevel::All, + emojis: HashMap::new(), + explicit_content_filter: ExplicitContentFilter::None, + features: vec![], + icon: None, + joined_at: datetime, + large: false, + member_count: 0, + members: HashMap::new(), + mfa_level: MfaLevel::None, + name: String::new(), + owner_id: UserId(3), + presences: HashMap::new(), + region: String::new(), + roles: HashMap::new(), + splash: None, + system_channel_id: None, + verification_level: VerificationLevel::Low, + voice_states: HashMap::new(), + channels, + }, + } + }; + assert!(cache.update(&mut guild_create).is_none()); + assert!(cache.update(&mut event).is_none()); + + let mut guild_delete = GuildDeleteEvent { + guild: PartialGuild { + id: GuildId(1), + afk_channel_id: None, + afk_timeout: 0, + default_message_notifications: DefaultMessageNotificationLevel::All, + embed_channel_id: None, + embed_enabled: false, + emojis: HashMap::new(), + features: vec![], + icon: None, + mfa_level: MfaLevel::None, + name: String::new(), + owner_id: UserId(3), + region: String::new(), + roles: HashMap::new(), + splash: None, + verification_level: VerificationLevel::Low, + }, + }; + + // The guild existed in the cache, so the cache's guild is returned by the + // update. + assert!(cache.update(&mut guild_delete).is_some()); + + // Assert that the channel's message cache no longer exists. + assert!(!cache.messages.contains_key(&ChannelId(2))); + } +} diff --git a/src/client/bridge/gateway/shard_messenger.rs b/src/client/bridge/gateway/shard_messenger.rs index 2331d4a..6d625b6 100644 --- a/src/client/bridge/gateway/shard_messenger.rs +++ b/src/client/bridge/gateway/shard_messenger.rs @@ -157,7 +157,11 @@ impl ShardMessenger { /// # try_main().unwrap(); /// # } /// ``` - pub fn set_game(&self, game: Option<Game>) { + pub fn set_game<T: Into<Game>>(&self, game: Option<T>) { + self._set_game(game.map(Into::into)) + } + + fn _set_game(&self, game: Option<Game>) { let _ = self.send(ShardRunnerMessage::SetGame(game)); } @@ -195,7 +199,15 @@ impl ShardMessenger { /// # try_main().unwrap(); /// # } /// ``` - pub fn set_presence(&self, game: Option<Game>, mut status: OnlineStatus) { + pub fn set_presence<T: Into<Game>>( + &self, + game: Option<T>, + status: OnlineStatus, + ) { + self._set_presence(game.map(Into::into), status) + } + + fn _set_presence(&self, game: Option<Game>, mut status: OnlineStatus) { if status == OnlineStatus::Offline { status = OnlineStatus::Invisible; } diff --git a/src/client/context.rs b/src/client/context.rs index d581ffb..61a1925 100644 --- a/src/client/context.rs +++ b/src/client/context.rs @@ -87,6 +87,7 @@ impl Context { /// client.start().unwrap(); /// ``` #[cfg(feature = "builder")] + #[deprecated(since = "0.5.6", note = "Use the http module instead.")] pub fn edit_profile<F: FnOnce(EditProfile) -> EditProfile>(&self, f: F) -> Result<CurrentUser> { let mut map = VecMap::with_capacity(2); @@ -273,7 +274,7 @@ impl Context { /// [`set_presence`]: #method.set_presence #[inline] pub fn reset_presence(&self) { - self.shard.set_presence(None, OnlineStatus::Online); + self.shard.set_presence(None::<Game>, OnlineStatus::Online); } /// Sets the current game, defaulting to an online status of [`Online`]. @@ -316,7 +317,11 @@ impl Context { /// /// [`Online`]: ../model/user/enum.OnlineStatus.html#variant.Online #[inline] - pub fn set_game(&self, game: Game) { + pub fn set_game<T: Into<Game>>(&self, game: T) { + self._set_game(game.into()) + } + + fn _set_game(&self, game: Game) { self.shard.set_presence(Some(game), OnlineStatus::Online); } @@ -356,14 +361,10 @@ impl Context { /// [`Playing`]: ../model/gateway/enum.GameType.html#variant.Playing /// [`reset_presence`]: #method.reset_presence /// [`set_presence`]: #method.set_presence - pub fn set_game_name(&self, game_name: &str) { - let game = Game { - kind: GameType::Playing, - name: game_name.to_string(), - url: None, - }; - - self.shard.set_presence(Some(game), OnlineStatus::Online); + #[deprecated(since = "0.5.5", note = "Use Context::set_game")] + #[inline] + pub fn set_game_name<T: Into<String>>(&self, game_name: T) { + self.set_game(game_name.into()) } /// Sets the current user's presence, providing all fields to be passed. diff --git a/src/client/dispatch.rs b/src/client/dispatch.rs index dd5b4fe..663a283 100644 --- a/src/client/dispatch.rs +++ b/src/client/dispatch.rs @@ -15,14 +15,10 @@ use std::sync::mpsc::Sender; use threadpool::ThreadPool; use typemap::ShareMap; -#[cfg(feature = "cache")] -use chrono::{Timelike, Utc}; #[cfg(feature = "framework")] use framework::Framework; #[cfg(feature = "cache")] use model::id::GuildId; -#[cfg(feature = "cache")] -use std::{thread, time}; #[cfg(feature = "cache")] use super::CACHE; @@ -38,11 +34,6 @@ macro_rules! update { }; } -#[cfg(feature = "cache")] -macro_rules! now { - () => (Utc::now().time().second() * 1000) -} - fn context( data: &Arc<Mutex<ShareMap>>, runner_tx: &Sender<InterMessage>, @@ -149,20 +140,6 @@ fn handle_event<H: EventHandler + Send + Sync + 'static>( threadpool: &ThreadPool, shard_id: u64, ) { - #[cfg(feature = "cache")] - let mut last_guild_create_time = now!(); - - #[cfg(feature = "cache")] - let wait_for_guilds = move || -> ::Result<()> { - let unavailable_guilds = CACHE.read().unavailable_guilds.len(); - - while unavailable_guilds != 0 && (now!() < last_guild_create_time + 2000) { - thread::sleep(time::Duration::from_millis(500)); - } - - Ok(()) - }; - match event { DispatchEvent::Client(ClientEvent::ShardStageUpdate(event)) => { let context = context(data, runner_tx, shard_id); @@ -177,9 +154,9 @@ fn handle_event<H: EventHandler + Send + Sync + 'static>( let context = context(data, runner_tx, shard_id); - // This different channel_create dispatching is only due to the fact that - // each time the bot receives a dm, this event is also fired. - // So in short, only exists to reduce unnecessary clutter. + // Discord sends both a MessageCreate and a ChannelCreate upon a new message in a private channel. + // This could potentionally be annoying to handle when otherwise wanting to normally take care of a new channel. + // So therefore, private channels are dispatched to their own handler code. match event.channel { Channel::Private(channel) => { let event_handler = Arc::clone(event_handler); @@ -309,8 +286,6 @@ fn handle_event<H: EventHandler + Send + Sync + 'static>( #[cfg(feature = "cache")] { - last_guild_create_time = now!(); - let cache = CACHE.read(); if cache.unavailable_guilds.is_empty() { @@ -578,28 +553,12 @@ fn handle_event<H: EventHandler + Send + Sync + 'static>( DispatchEvent::Model(Event::Ready(mut event)) => { update!(event); - let event_handler = Arc::clone(event_handler); - - feature_cache! {{ - last_guild_create_time = now!(); - - let _ = wait_for_guilds() - .map(move |_| { - let context = context(data, runner_tx, shard_id); - let event_handler = Arc::clone(&event_handler); - - threadpool.execute(move || { - event_handler.ready(context, event.ready); - }); - }); - } else { - let context = context(data, runner_tx, shard_id); - let event_handler = Arc::clone(&event_handler); + let context = context(data, runner_tx, shard_id); + let event_handler = Arc::clone(&event_handler); - threadpool.execute(move || { - event_handler.ready(context, event.ready); - }); - }} + threadpool.execute(move || { + event_handler.ready(context, event.ready); + }); }, DispatchEvent::Model(Event::Resumed(mut event)) => { let context = context(data, runner_tx, shard_id); diff --git a/src/framework/standard/args.rs b/src/framework/standard/args.rs index bf3056b..0b915b6 100644 --- a/src/framework/standard/args.rs +++ b/src/framework/standard/args.rs @@ -944,3 +944,436 @@ fn quotes_extract(token: &Token) -> &str { &token.lit } } + +#[cfg(test)] +mod test { + use super::{Args, Error as ArgError}; + + #[test] + fn single_with_empty_message() { + let mut args = Args::new("", &["".to_string()]); + assert_matches!(args.single::<String>().unwrap_err(), ArgError::Eos); + + let mut args = Args::new("", &[",".to_string()]); + assert_matches!(args.single::<String>().unwrap_err(), ArgError::Eos); + } + + #[test] + fn single_n_with_empty_message() { + let args = Args::new("", &["".to_string()]); + assert_matches!(args.single_n::<String>().unwrap_err(), ArgError::Eos); + + let args = Args::new("", &[",".to_string()]); + assert_matches!(args.single_n::<String>().unwrap_err(), ArgError::Eos); + } + + #[test] + fn single_quoted_with_empty_message() { + let mut args = Args::new("", &["".to_string()]); + assert_matches!(args.single_quoted::<String>().unwrap_err(), ArgError::Eos); + + let mut args = Args::new("", &[",".to_string()]); + assert_matches!(args.single_quoted::<String>().unwrap_err(), ArgError::Eos); + } + + #[test] + fn multiple_with_empty_message() { + let args = Args::new("", &["".to_string()]); + assert_matches!(args.multiple::<String>().unwrap_err(), ArgError::Eos); + + let args = Args::new("", &[",".to_string()]); + assert_matches!(args.multiple::<String>().unwrap_err(), ArgError::Eos); + } + + #[test] + fn multiple_quoted_with_empty_message() { + let args = Args::new("", &["".to_string()]); + assert_matches!(args.multiple_quoted::<String>().unwrap_err(), ArgError::Eos); + + let args = Args::new("", &[",".to_string()]); + assert_matches!(args.multiple_quoted::<String>().unwrap_err(), ArgError::Eos); + } + + #[test] + fn skip_with_empty_message() { + let mut args = Args::new("", &["".to_string()]); + assert_matches!(args.skip(), None); + + let mut args = Args::new("", &[",".to_string()]); + assert_matches!(args.skip(), None); + } + + #[test] + fn skip_for_with_empty_message() { + let mut args = Args::new("", &["".to_string()]); + assert_matches!(args.skip_for(0), None); + + let mut args = Args::new("", &["".to_string()]); + assert_matches!(args.skip_for(5), None); + + let mut args = Args::new("", &[",".to_string()]); + assert_matches!(args.skip_for(0), None); + + let mut args = Args::new("", &[",".to_string()]); + assert_matches!(args.skip_for(5), None); + } + + #[test] + fn single_i32_with_2_bytes_long_delimiter() { + let mut args = Args::new("1, 2", &[", ".to_string()]); + + assert_eq!(args.single::<i32>().unwrap(), 1); + assert_eq!(args.single::<i32>().unwrap(), 2); + } + + #[test] + fn single_i32_with_1_byte_long_delimiter_i32() { + let mut args = Args::new("1,2", &[",".to_string()]); + + assert_eq!(args.single::<i32>().unwrap(), 1); + assert_eq!(args.single::<i32>().unwrap(), 2); + } + + #[test] + fn single_i32_with_wrong_char_after_first_arg() { + let mut args = Args::new("1, 2", &[",".to_string()]); + + assert_eq!(args.single::<i32>().unwrap(), 1); + assert!(args.single::<i32>().is_err()); + } + + #[test] + fn single_i32_with_one_character_being_3_bytes_long() { + let mut args = Args::new("1★2", &["★".to_string()]); + + assert_eq!(args.single::<i32>().unwrap(), 1); + assert_eq!(args.single::<i32>().unwrap(), 2); + } + + #[test] + fn single_i32_with_untrimmed_whitespaces() { + let mut args = Args::new(" 1, 2 ", &[",".to_string()]); + + assert!(args.single::<i32>().is_err()); + } + + #[test] + fn single_i32_n() { + let args = Args::new("1,2", &[",".to_string()]); + + assert_eq!(args.single_n::<i32>().unwrap(), 1); + assert_eq!(args.single_n::<i32>().unwrap(), 1); + } + + #[test] + fn single_quoted_chaining() { + let mut args = Args::new(r#""1, 2" "2" """#, &[" ".to_string()]); + + assert_eq!(args.single_quoted::<String>().unwrap(), "1, 2"); + assert_eq!(args.single_quoted::<String>().unwrap(), "2"); + assert_eq!(args.single_quoted::<String>().unwrap(), ""); + } + + #[test] + fn single_quoted_and_single_chaining() { + let mut args = Args::new(r#""1, 2" "2" "3" 4"#, &[" ".to_string()]); + + assert_eq!(args.single_quoted::<String>().unwrap(), "1, 2"); + assert!(args.single_n::<i32>().is_err()); + assert_eq!(args.single::<String>().unwrap(), "\"2\""); + assert_eq!(args.single_quoted::<i32>().unwrap(), 3); + assert_eq!(args.single::<i32>().unwrap(), 4); + } + + #[test] + fn full_on_args() { + let test_text = "Some text to ensure `full()` works."; + let args = Args::new(test_text, &[" ".to_string()]); + + assert_eq!(args.full(), test_text); + } + + #[test] + fn multiple_quoted_strings_one_delimiter() { + let args = Args::new(r#""1, 2" "a" "3" 4 "5"#, &[" ".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["1, 2", "a", "3", "4", "\"5"]); + } + + #[test] + fn multiple_quoted_strings_with_multiple_delimiter() { + let args = Args::new(r#""1, 2" "a","3"4 "5"#, &[" ".to_string(), ",".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["1, 2", "a", "3", "4", "\"5"]); + } + + #[test] + fn multiple_quoted_strings_with_multiple_delimiters() { + let args = Args::new(r#""1, 2" "a","3" """#, &[" ".to_string(), ",".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["1, 2", "a", "3", ""]); + } + + #[test] + fn multiple_quoted_i32() { + let args = Args::new(r#""1" "2" 3"#, &[" ".to_string()]); + + assert_eq!(args.multiple_quoted::<i32>().unwrap(), [1, 2, 3]); + } + + #[test] + fn multiple_quoted_quote_appears_without_delimiter_in_front() { + let args = Args::new(r#"hello, my name is cake" 2"#, &[",".to_string(), " ".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello", "my", "name", "is", "cake\"", "2"]); + } + + #[test] + fn multiple_quoted_single_quote() { + let args = Args::new(r#"hello "2 b"#, &[",".to_string(), " ".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello", "\"2 b"]); + } + + #[test] + fn multiple_quoted_one_quote_pair() { + let args = Args::new(r#"hello "2 b""#, &[",".to_string(), " ".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello", "2 b"]); + } + + + #[test] + fn delimiter_before_multiple_quoted() { + let args = Args::new(r#","hello, my name is cake" "2""#, &[",".to_string(), " ".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello, my name is cake", "2"]); + } + + #[test] + fn no_quote() { + let args = Args::new("hello, my name is cake", &[",".to_string(), " ".to_string()]); + + assert_eq!(args.single_quoted_n::<String>().unwrap(), "hello"); + } + + #[test] + fn single_quoted_n() { + let args = Args::new(r#""hello, my name is cake","test"#, &[",".to_string()]); + + assert_eq!(args.single_quoted_n::<String>().unwrap(), "hello, my name is cake"); + assert_eq!(args.single_quoted_n::<String>().unwrap(), "hello, my name is cake"); + } + + #[test] + fn multiple_quoted_starting_with_wrong_delimiter_in_first_quote() { + let args = Args::new(r#""hello, my name is cake" "2""#, &[",".to_string(), " ".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello, my name is cake", "2"]); + } + + #[test] + fn multiple_quoted_with_one_correct_and_one_invalid_quote() { + let args = Args::new(r#""hello, my name is cake" "2""#, &[",".to_string(), " ".to_string()]); + + assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello, my name is cake", "2"]); + } + + #[test] + fn find_i32_one_one_byte_delimiter() { + let mut args = Args::new("hello,my name is cake 2", &[" ".to_string()]); + + assert_eq!(args.find::<i32>().unwrap(), 2); + } + + #[test] + fn find_i32_one_three_byte_delimiter() { + let mut args = Args::new("hello,my name is cakeé2", &["é".to_string()]); + + assert_eq!(args.find::<i32>().unwrap(), 2); + } + + #[test] + fn find_i32_multiple_delimiter_but_i32_not_last() { + let mut args = Args::new("hello,my name is 2 cake", &[" ".to_string(), ",".to_string()]); + + assert_eq!(args.find::<i32>().unwrap(), 2); + } + + #[test] + fn find_i32_multiple_delimiter() { + let mut args = Args::new("hello,my name is cake 2", &[" ".to_string(), ",".to_string()]); + + assert_eq!(args.find::<i32>().unwrap(), 2); + } + + #[test] + fn find_n_i32() { + let mut args = Args::new("a 2", &[" ".to_string()]); + + assert_eq!(args.find_n::<i32>().unwrap(), 2); + assert_eq!(args.find_n::<i32>().unwrap(), 2); + } + + #[test] + fn skip() { + let mut args = Args::new("1 2", &[" ".to_string()]); + + assert_eq!(args.skip().unwrap(), "1"); + assert_eq!(args.remaining(), 1); + assert_eq!(args.single::<String>().unwrap(), "2"); + } + + #[test] + fn skip_for() { + let mut args = Args::new("1 2 neko 100", &[" ".to_string()]); + + assert_eq!(args.skip_for(2).unwrap(), ["1", "2"]); + assert_eq!(args.remaining(), 2); + assert_eq!(args.single::<String>().unwrap(), "neko"); + assert_eq!(args.single::<String>().unwrap(), "100"); + } + + #[test] + fn len_with_one_delimiter() { + let args = Args::new("1 2 neko 100", &[" ".to_string()]); + + assert_eq!(args.len(), 4); + assert_eq!(args.remaining(), 4); + } + + #[test] + fn len_multiple_quoted() { + let args = Args::new(r#""hello, my name is cake" "2""#, &[" ".to_string()]); + + assert_eq!(args.len(), 2); + } + + #[test] + fn remaining_len_before_and_after_single() { + let mut args = Args::new("1 2", &[" ".to_string()]); + + assert_eq!(args.remaining(), 2); + assert_eq!(args.single::<i32>().unwrap(), 1); + assert_eq!(args.remaining(), 1); + assert_eq!(args.single::<i32>().unwrap(), 2); + assert_eq!(args.remaining(), 0); + } + + #[test] + fn remaining_len_before_and_after_single_quoted() { + let mut args = Args::new(r#""1" "2" "3""#, &[" ".to_string()]); + + assert_eq!(args.remaining(), 3); + assert_eq!(args.single_quoted::<i32>().unwrap(), 1); + assert_eq!(args.remaining(), 2); + assert_eq!(args.single_quoted::<i32>().unwrap(), 2); + assert_eq!(args.remaining(), 1); + assert_eq!(args.single_quoted::<i32>().unwrap(), 3); + assert_eq!(args.remaining(), 0); + } + + #[test] + fn remaining_len_before_and_after_skip() { + let mut args = Args::new("1 2", &[" ".to_string()]); + + assert_eq!(args.remaining(), 2); + assert_eq!(args.skip().unwrap(), "1"); + assert_eq!(args.remaining(), 1); + assert_eq!(args.skip().unwrap(), "2"); + assert_eq!(args.remaining(), 0); + } + + #[test] + fn remaining_len_before_and_after_skip_empty_string() { + let mut args = Args::new("", &[" ".to_string()]); + + assert_eq!(args.remaining(), 0); + assert_eq!(args.skip(), None); + assert_eq!(args.remaining(), 0); + } + + #[test] + fn remaining_len_before_and_after_skip_for() { + let mut args = Args::new("1 2", &[" ".to_string()]); + + assert_eq!(args.remaining(), 2); + assert_eq!(args.skip_for(2), Some(vec!["1".to_string(), "2".to_string()])); + assert_eq!(args.skip_for(2), None); + assert_eq!(args.remaining(), 0); + } + + #[test] + fn remaining_len_before_and_after_find() { + let mut args = Args::new("a 2 6", &[" ".to_string()]); + + assert_eq!(args.remaining(), 3); + assert_eq!(args.find::<i32>().unwrap(), 2); + assert_eq!(args.remaining(), 2); + assert_eq!(args.find::<i32>().unwrap(), 6); + assert_eq!(args.remaining(), 1); + assert_eq!(args.find::<String>().unwrap(), "a"); + assert_eq!(args.remaining(), 0); + assert_matches!(args.find::<String>().unwrap_err(), ArgError::Eos); + assert_eq!(args.remaining(), 0); + } + + #[test] + fn remaining_len_before_and_after_find_n() { + let mut args = Args::new("a 2 6", &[" ".to_string()]); + + assert_eq!(args.remaining(), 3); + assert_eq!(args.find_n::<i32>().unwrap(), 2); + assert_eq!(args.remaining(), 3); + } + + + #[test] + fn multiple_strings_with_one_delimiter() { + let args = Args::new("hello, my name is cake 2", &[" ".to_string()]); + + assert_eq!(args.multiple::<String>().unwrap(), ["hello,", "my", "name", "is", "cake", "2"]); + } + + #[test] + fn multiple_i32_with_one_delimiter() { + let args = Args::new("1 2 3", &[" ".to_string()]); + + assert_eq!(args.multiple::<i32>().unwrap(), [1, 2, 3]); + } + + #[test] + fn multiple_i32_with_one_delimiter_and_parse_error() { + let args = Args::new("1 2 3 abc", &[" ".to_string()]); + + assert_matches!(args.multiple::<i32>().unwrap_err(), ArgError::Parse(_)); + } + + #[test] + fn multiple_i32_with_three_delimiters() { + let args = Args::new("1 2 3", &[" ".to_string(), ",".to_string()]); + + assert_eq!(args.multiple::<i32>().unwrap(), [1, 2, 3]); + } + + #[test] + fn single_after_failed_single() { + let mut args = Args::new("b 2", &[" ".to_string()]); + + assert_matches!(args.single::<i32>().unwrap_err(), ArgError::Parse(_)); + // Test that `single` short-circuts on an error and leaves the source as is. + assert_eq!(args.remaining(), 2); + assert_eq!(args.single::<String>().unwrap(), "b"); + assert_eq!(args.single::<String>().unwrap(), "2"); + } + + #[test] + fn remaining_len_after_failed_single_quoted() { + let mut args = Args::new("b a", &[" ".to_string()]); + + assert_eq!(args.remaining(), 2); + // Same goes for `single_quoted` and the alike. + assert_matches!(args.single_quoted::<i32>().unwrap_err(), ArgError::Parse(_)); + assert_eq!(args.remaining(), 2); + } +} diff --git a/src/framework/standard/help_commands.rs b/src/framework/standard/help_commands.rs index fd74ace..e891ff5 100644 --- a/src/framework/standard/help_commands.rs +++ b/src/framework/standard/help_commands.rs @@ -266,7 +266,7 @@ pub fn with_embeds<H: BuildHasher>( &help_options.striked_commands_tip_in_dm }; - if let Some(ref striked_command_text) = striked_command_tip { + if let &Some(ref striked_command_text) = striked_command_tip { e = e.colour(help_options.embed_success_colour).description( format!("{}\n{}", &help_options.individual_command_tip, striked_command_text), ); @@ -510,7 +510,7 @@ pub fn plain<H: BuildHasher>( &help_options.striked_commands_tip_in_dm }; - if let Some(ref striked_command_text) = striked_command_tip { + if let &Some(ref striked_command_text) = striked_command_tip { let _ = writeln!(result, "{}\n{}\n", &help_options.individual_command_tip, striked_command_text); } else { let _ = writeln!(result, "{}\n", &help_options.individual_command_tip); diff --git a/src/framework/standard/mod.rs b/src/framework/standard/mod.rs index d947bfd..0bb0309 100644 --- a/src/framework/standard/mod.rs +++ b/src/framework/standard/mod.rs @@ -325,12 +325,10 @@ impl StandardFramework { /// /// client.with_framework(StandardFramework::new() /// .complex_bucket("basic", 2, 10, 3, |_, guild_id, channel_id, user_id| { - /// // check if the guild is `123` and the channel where the command(s) was called: - /// // `456` - /// // and if the user who called the command(s) is `789` - /// // otherwise don't apply the bucket at all. - /// guild_id.is_some() && guild_id.unwrap() == 123 && channel_id == 456 - /// && user_id == 789 + /// // Our bucket is very strict. It cannot apply in DMs. + /// // And can only apply if it's in the specific guild, channel and by the specific user. + /// guild_id.is_some() && guild_id.unwrap() == 123 && channel_id == 456 + /// && user_id == 789 /// }) /// .command("ping", |c| c /// .bucket("basic") @@ -338,7 +336,9 @@ impl StandardFramework { /// msg.channel_id.say("pong!")?; /// /// Ok(()) - /// }))); + /// }) + /// ) + /// ); /// ``` /// /// [`bucket`]: #method.bucket @@ -384,14 +384,18 @@ impl StandardFramework { /// /// client.with_framework(StandardFramework::new() /// .complex_bucket("basic", 2, 10, 3, |_, channel_id, user_id| { - /// // check if the channel's id where the command(s) was called is `456` - /// // and if the user who called the command(s) is `789` - /// // otherwise don't apply the bucket at all. + /// Our bucket is somewhat strict. It can only apply in the specific channel and by the specific user. /// channel_id == 456 && user_id == 789 /// }) /// .command("ping", |c| c /// .bucket("basic") - /// .exec_str("pong!"))); + /// .exec(|_, msg, _| { + /// msg.channel_id.say("pong!")?; + /// + /// Ok(()) + /// }) + /// ) + /// ); /// ``` /// /// [`bucket`]: #method.bucket @@ -1142,7 +1146,7 @@ impl Framework for StandardFramework { } if check_contains_group_prefix { - if let Some(CommandOrAlias::Command(ref command)) = &group.default_command { + if let &Some(CommandOrAlias::Command(ref command)) = &group.default_command { let command = Arc::clone(command); threadpool.execute(move || { @@ -1172,7 +1176,7 @@ impl Framework for StandardFramework { } } - if let Some(unrecognised_command) = &self.unrecognised_command { + if let &Some(ref unrecognised_command) = &self.unrecognised_command { let unrecognised_command = unrecognised_command.clone(); threadpool.execute(move || { (unrecognised_command)(&mut context, &message, &unrecognised_command_name); diff --git a/src/http/mod.rs b/src/http/mod.rs index 71c57c6..c50679a 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -2008,3 +2008,21 @@ pub enum GuildPagination { /// The Id to get the guilds before. Before(GuildId), } + +#[cfg(test)] +mod test { + use super::AttachmentType; + use std::path::Path; + + #[test] + fn test_attachment_type() { + assert!(match AttachmentType::from(Path::new("./dogs/corgis/kona.png")) { + AttachmentType::Path(_) => true, + _ => false, + }); + assert!(match AttachmentType::from("./cats/copycat.png") { + AttachmentType::Path(_) => true, + _ => false, + }); + } +} @@ -140,6 +140,10 @@ extern crate typemap; #[cfg(feature = "evzht9h3nznqzwl")] extern crate evzht9h3nznqzwl as websocket; +#[cfg(test)] +#[macro_use] +extern crate matches; + #[macro_use] mod internal; diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 48003ef..ceb05c3 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -557,9 +557,9 @@ impl ChannelId { /// over the limit. /// /// [`Channel`]: enum.Channel.html - /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong - /// [`CreateMessage`]: ../builder/struct.CreateMessage.html - /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + /// [`ModelError::MessageTooLong`]: ../error/enum.Error.html#variant.MessageTooLong + /// [`CreateMessage`]: ../../builder/struct.CreateMessage.html + /// [Send Messages]: ../permissions/struct.Permissions.html#associatedconstant.SEND_MESSAGES #[cfg(feature = "utils")] pub fn send_message<F>(&self, f: F) -> Result<Message> where F: FnOnce(CreateMessage) -> CreateMessage { diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs index 8d586a6..e0ef085 100644 --- a/src/model/channel/mod.rs +++ b/src/model/channel/mod.rs @@ -677,3 +677,93 @@ pub enum PermissionOverwriteType { /// A role which is having its permission overwrites edited. Role(RoleId), } + +#[cfg(test)] +mod test { + #[cfg(feature = "utils")] + mod utils { + use model::prelude::*; + use parking_lot::RwLock; + use std::collections::HashMap; + use std::sync::Arc; + + fn group() -> Group { + Group { + channel_id: ChannelId(1), + icon: None, + last_message_id: None, + last_pin_timestamp: None, + name: None, + owner_id: UserId(2), + recipients: HashMap::new(), + } + } + + fn guild_channel() -> GuildChannel { + GuildChannel { + id: ChannelId(1), + bitrate: None, + category_id: None, + guild_id: GuildId(2), + kind: ChannelType::Text, + last_message_id: None, + last_pin_timestamp: None, + name: "nsfw-stuff".to_string(), + permission_overwrites: vec![], + position: 0, + topic: None, + user_limit: None, + nsfw: false, + } + } + + fn private_channel() -> PrivateChannel { + PrivateChannel { + id: ChannelId(1), + last_message_id: None, + last_pin_timestamp: None, + kind: ChannelType::Private, + recipient: Arc::new(RwLock::new(User { + id: UserId(2), + avatar: None, + bot: false, + discriminator: 1, + name: "ab".to_string(), + })), + } + } + + #[test] + fn nsfw_checks() { + let mut channel = guild_channel(); + assert!(channel.is_nsfw()); + channel.kind = ChannelType::Voice; + assert!(!channel.is_nsfw()); + + channel.kind = ChannelType::Text; + channel.name = "nsfw-".to_string(); + assert!(!channel.is_nsfw()); + + channel.name = "nsfw".to_string(); + assert!(channel.is_nsfw()); + channel.kind = ChannelType::Voice; + assert!(!channel.is_nsfw()); + channel.kind = ChannelType::Text; + + channel.name = "nsf".to_string(); + channel.nsfw = true; + assert!(channel.is_nsfw()); + channel.nsfw = false; + assert!(!channel.is_nsfw()); + + let channel = Channel::Guild(Arc::new(RwLock::new(channel))); + assert!(!channel.is_nsfw()); + + let group = group(); + assert!(!group.is_nsfw()); + + let private_channel = private_channel(); + assert!(!private_channel.is_nsfw()); + } + } +} diff --git a/src/model/gateway.rs b/src/model/gateway.rs index b73d900..9af4a7e 100644 --- a/src/model/gateway.rs +++ b/src/model/gateway.rs @@ -131,6 +131,58 @@ impl Game { } } +impl<'a> From<&'a str> for Game { + fn from(name: &'a str) -> Self { + Game::playing(name) + } +} + +impl From<String> for Game { + fn from(name: String) -> Self { + Game::playing(&name) + } +} + +impl<'a> From<(String, GameType)> for Game { + fn from((name, kind): (String, GameType)) -> Self { + Self { + url: None, + kind, + name, + } + } +} + +impl<'a> From<(&'a str, &'a str)> for Game { + fn from((name, url): (&'a str, &'a str)) -> Self { + Self { + kind: GameType::Streaming, + name: name.to_owned(), + url: Some(url.to_owned()), + } + } +} + +impl From<(String, String)> for Game { + fn from((name, url): (String, String)) -> Self { + Self { + kind: GameType::Streaming, + url: Some(url), + name, + } + } +} + +impl From<(String, GameType, String)> for Game { + fn from((name, kind, url): (String, GameType, String)) -> Self { + Self { + url: Some(url), + kind, + name, + } + } +} + impl<'de> Deserialize<'de> for Game { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { let mut map = JsonMap::deserialize(deserializer)?; diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 9c3120a..ebe930e 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -444,7 +444,10 @@ impl GuildId { #[inline] pub fn leave(&self) -> Result<()> { http::leave_guild(self.0) } - /// Gets a user's [`Member`] for the guild by Id. + /// Gets a user's [`Member`] for the guild by Id. + /// + /// If the cache feature is enabled the cache will be checked + /// first. If not found it will resort to an http request. /// /// [`Guild`]: struct.Guild.html /// [`Member`]: struct.Member.html @@ -454,6 +457,13 @@ impl GuildId { } fn _member(&self, user_id: UserId) -> Result<Member> { + #[cfg(feature = "cache")] + { + if let Some(member) = CACHE.read().member(self.0, user_id) { + return Ok(member); + } + } + http::get_member(self.0, user_id.0) } diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index 67b0f0e..30651d6 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -2015,3 +2015,109 @@ impl VerificationLevel { } } } + +#[cfg(test)] +mod test { + #[cfg(feature = "model")] + mod model { + use chrono::prelude::*; + use model::prelude::*; + use std::collections::*; + use std::sync::Arc; + + fn gen_user() -> User { + User { + id: UserId(210), + avatar: Some("abc".to_string()), + bot: true, + discriminator: 1432, + name: "test".to_string(), + } + } + + fn gen_member() -> Member { + let dt: DateTime<FixedOffset> = FixedOffset::east(5 * 3600) + .ymd(2016, 11, 08) + .and_hms(0, 0, 0); + let vec1 = Vec::new(); + let u = Arc::new(RwLock::new(gen_user())); + + Member { + deaf: false, + guild_id: GuildId(1), + joined_at: Some(dt), + mute: false, + nick: Some("aaaa".to_string()), + roles: vec1, + user: u, + } + } + + fn gen() -> Guild { + let u = gen_user(); + let m = gen_member(); + + let hm1 = HashMap::new(); + let hm2 = HashMap::new(); + let vec1 = Vec::new(); + let dt: DateTime<FixedOffset> = FixedOffset::east(5 * 3600) + .ymd(2016, 11, 08) + .and_hms(0, 0, 0); + let mut hm3 = HashMap::new(); + let hm4 = HashMap::new(); + let hm5 = HashMap::new(); + let hm6 = HashMap::new(); + + hm3.insert(u.id, m); + + let notifications = DefaultMessageNotificationLevel::All; + + Guild { + afk_channel_id: Some(ChannelId(0)), + afk_timeout: 0, + channels: hm1, + default_message_notifications: notifications, + emojis: hm2, + features: vec1, + icon: Some("/avatars/210/a_aaa.webp?size=1024".to_string()), + id: GuildId(1), + joined_at: dt, + large: false, + member_count: 1, + members: hm3, + mfa_level: MfaLevel::Elevated, + name: "Spaghetti".to_string(), + owner_id: UserId(210), + presences: hm4, + region: "NA".to_string(), + roles: hm5, + splash: Some("asdf".to_string()), + verification_level: VerificationLevel::None, + voice_states: hm6, + application_id: Some(ApplicationId(0)), + explicit_content_filter: ExplicitContentFilter::None, + system_channel_id: Some(ChannelId(0)), + } + } + + + #[test] + fn member_named_username() { + let guild = gen(); + let lhs = guild + .member_named("test#1432") + .unwrap() + .display_name(); + + assert_eq!(lhs, gen_member().display_name()); + } + + #[test] + fn member_named_nickname() { + let guild = gen(); + let lhs = guild.member_named("aaaa").unwrap().display_name(); + + assert_eq!(lhs, gen_member().display_name()); + } + } +} diff --git a/src/model/misc.rs b/src/model/misc.rs index 3195c55..62a002a 100644 --- a/src/model/misc.rs +++ b/src/model/misc.rs @@ -308,3 +308,83 @@ pub struct Maintenance { pub start: String, pub stop: String, } + +#[cfg(test)] +mod test { + use model::prelude::*; + use parking_lot::RwLock; + use std::sync::Arc; + use utils::Colour; + + #[test] + fn test_formatters() { + assert_eq!(ChannelId(1).to_string(), "1"); + assert_eq!(EmojiId(2).to_string(), "2"); + assert_eq!(GuildId(3).to_string(), "3"); + assert_eq!(RoleId(4).to_string(), "4"); + assert_eq!(UserId(5).to_string(), "5"); + } + + #[cfg(feature = "utils")] + #[test] + fn test_mention() { + let channel = Channel::Guild(Arc::new(RwLock::new(GuildChannel { + bitrate: None, + category_id: None, + guild_id: GuildId(1), + kind: ChannelType::Text, + id: ChannelId(4), + last_message_id: None, + last_pin_timestamp: None, + name: "a".to_string(), + permission_overwrites: vec![], + position: 1, + topic: None, + user_limit: None, + nsfw: false, + }))); + let emoji = Emoji { + animated: false, + id: EmojiId(5), + name: "a".to_string(), + managed: true, + require_colons: true, + roles: vec![], + }; + let role = Role { + id: RoleId(2), + colour: Colour::ROSEWATER, + hoist: false, + managed: false, + mentionable: false, + name: "fake role".to_string(), + permissions: Permissions::empty(), + position: 1, + }; + let user = User { + id: UserId(6), + avatar: None, + bot: false, + discriminator: 4132, + name: "fake".to_string(), + }; + let member = Member { + deaf: false, + guild_id: GuildId(2), + joined_at: None, + mute: false, + nick: None, + roles: vec![], + user: Arc::new(RwLock::new(user.clone())), + }; + + assert_eq!(ChannelId(1).mention(), "<#1>"); + assert_eq!(channel.mention(), "<#4>"); + assert_eq!(emoji.mention(), "<:a:5>"); + assert_eq!(member.mention(), "<@6>"); + assert_eq!(role.mention(), "<@&2>"); + assert_eq!(role.id.mention(), "<@&2>"); + assert_eq!(user.mention(), "<@6>"); + assert_eq!(user.id.mention(), "<@6>"); + } +} diff --git a/src/model/user.rs b/src/model/user.rs index a378be5..d76b135 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -738,9 +738,10 @@ impl UserId { #[cfg(feature = "cache")] pub fn find(&self) -> Option<Arc<RwLock<User>>> { CACHE.read().user(*self) } - /// Gets a user by its Id over the REST API. + /// Gets a user by its Id from either the cache or the REST API. /// - /// **Note**: The current user must be a bot user. + /// Searches the cache for the user first, if the cache is enabled. If the + /// user was not found, then the user is searched via the REST API. #[inline] pub fn get(&self) -> Result<User> { #[cfg(feature = "cache")] @@ -843,3 +844,71 @@ fn tag(name: &str, discriminator: u16) -> String { tag } + +#[cfg(test)] +mod test { + #[cfg(feature = "model")] + mod model { + use model::id::UserId; + use model::user::User; + + fn gen() -> User { + User { + id: UserId(210), + avatar: Some("abc".to_string()), + bot: true, + discriminator: 1432, + name: "test".to_string(), + } + } + + #[test] + fn test_core() { + let mut user = gen(); + + assert!( + user.avatar_url() + .unwrap() + .ends_with("/avatars/210/abc.webp?size=1024") + ); + assert!( + user.static_avatar_url() + .unwrap() + .ends_with("/avatars/210/abc.webp?size=1024") + ); + + user.avatar = Some("a_aaa".to_string()); + assert!( + user.avatar_url() + .unwrap() + .ends_with("/avatars/210/a_aaa.gif?size=1024") + ); + assert!( + user.static_avatar_url() + .unwrap() + .ends_with("/avatars/210/a_aaa.webp?size=1024") + ); + + user.avatar = None; + assert!(user.avatar_url().is_none()); + + assert_eq!(user.tag(), "test#1432"); + } + + #[test] + fn default_avatars() { + let mut user = gen(); + + user.discriminator = 0; + assert!(user.default_avatar_url().ends_with("0.png")); + user.discriminator = 1; + assert!(user.default_avatar_url().ends_with("1.png")); + user.discriminator = 2; + assert!(user.default_avatar_url().ends_with("2.png")); + user.discriminator = 3; + assert!(user.default_avatar_url().ends_with("3.png")); + user.discriminator = 4; + assert!(user.default_avatar_url().ends_with("4.png")); + } + } +} diff --git a/src/utils/colour.rs b/src/utils/colour.rs index b5c2e05..54e74b0 100644 --- a/src/utils/colour.rs +++ b/src/utils/colour.rs @@ -323,3 +323,55 @@ impl Default for Colour { /// Creates a default value for a `Colour`, setting the inner value to `0`. fn default() -> Colour { Colour(0) } } + +#[cfg(test)] +mod test { + use super::Colour; + use std::u32; + + #[test] + fn new() { + assert_eq!(Colour::new(1).0, 1); + assert_eq!(Colour::new(u32::MIN).0, u32::MIN); + assert_eq!(Colour::new(u32::MAX).0, u32::MAX); + } + + #[test] + fn from_rgb() { + assert_eq!(Colour::from_rgb(255, 0, 0).0, 0xFF0000); + assert_eq!(Colour::from_rgb(0, 255, 0).0, 0x00FF00); + assert_eq!(Colour::from_rgb(0, 0, 255).0, 0x0000FF); + } + + #[test] + fn r() { + assert_eq!(Colour::new(0x336123).r(), 0x33); + } + + #[test] + fn g() { + assert_eq!(Colour::new(0x336123).g(), 0x61); + } + + #[test] + fn b() { + assert_eq!(Colour::new(0x336123).b(), 0x23); + } + + #[test] + fn tuple() { + assert_eq!(Colour::new(0x336123).tuple(), (0x33, 0x61, 0x23)); + } + + #[test] + fn default() { + assert_eq!(Colour::default().0, 0); + } + + #[test] + fn from() { + assert_eq!(Colour::from(7i32).0, 7); + assert_eq!(Colour::from(7u32).0, 7); + assert_eq!(Colour::from(7u64).0, 7); + } +} diff --git a/src/utils/message_builder.rs b/src/utils/message_builder.rs index 7bbfddf..c609c28 100644 --- a/src/utils/message_builder.rs +++ b/src/utils/message_builder.rs @@ -988,3 +988,81 @@ fn normalize(text: &str) -> String { .replace("@everyone", "@\u{200B}everyone") .replace("@here", "@\u{200B}here") } + +#[cfg(test)] +mod test { + use model::prelude::*; + use super::{ + ContentModifier::*, + MessageBuilder, + }; + + #[test] + fn code_blocks() { + let content = MessageBuilder::new() + .push_codeblock("test", Some("rb")) + .build(); + assert_eq!(content, "```rb\ntest\n```"); + } + + #[test] + fn safe_content() { + let content = MessageBuilder::new() + .push_safe("@everyone discord.gg/discord-api") + .build(); + assert_ne!(content, "@everyone discord.gg/discord-api"); + } + + #[test] + fn no_free_formatting() { + let content = MessageBuilder::new().push_bold_safe("test**test").build(); + assert_ne!(content, "**test**test**"); + } + + #[test] + fn mentions() { + let content_emoji = MessageBuilder::new() + .emoji(&Emoji { + animated: false, + id: EmojiId(32), + name: "Rohrkatze".to_string(), + managed: false, + require_colons: true, + roles: vec![], + }) + .build(); + let content_mentions = MessageBuilder::new() + .channel(1) + .mention(&UserId(2)) + .role(3) + .user(4) + .build(); + assert_eq!(content_mentions, "<#1><@2><@&3><@4>"); + assert_eq!(content_emoji, "<:Rohrkatze:32>"); + } + + #[test] + fn content() { + let content = Bold + Italic + Code + "Fun!"; + + assert_eq!(content.to_string(), "***`Fun!`***"); + } + + #[test] + fn message_content() { + let message_content = MessageBuilder::new() + .push(Bold + Italic + Code + "Fun!") + .build(); + + assert_eq!(message_content, "***`Fun!`***"); + } + + #[test] + fn message_content_safe() { + let message_content = MessageBuilder::new() + .push_safe(Bold + Italic + "test**test") + .build(); + + assert_eq!(message_content, "***test\\*\\*test***"); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e20dd22..6c84384 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -500,3 +500,54 @@ pub fn with_cache_mut<T, F>(mut f: F) -> T let mut cache = CACHE.write(); f(&mut cache) } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_invite_parser() { + assert_eq!(parse_invite("https://discord.gg/abc"), "abc"); + assert_eq!(parse_invite("http://discord.gg/abc"), "abc"); + assert_eq!(parse_invite("discord.gg/abc"), "abc"); + } + + #[test] + fn test_username_parser() { + assert_eq!(parse_username("<@12345>").unwrap(), 12_345); + assert_eq!(parse_username("<@!12345>").unwrap(), 12_345); + } + + #[test] + fn role_parser() { + assert_eq!(parse_role("<@&12345>").unwrap(), 12_345); + } + + #[test] + fn test_channel_parser() { + assert_eq!(parse_channel("<#12345>").unwrap(), 12_345); + } + + #[test] + fn test_emoji_parser() { + let emoji = parse_emoji("<:name:12345>").unwrap(); + assert_eq!(emoji.name, "name"); + assert_eq!(emoji.id, 12_345); + } + + #[test] + fn test_quote_parser() { + let parsed = parse_quotes("a \"b c\" d\"e f\" g"); + assert_eq!(parsed, ["a", "b c", "d", "e f", "g"]); + } + + #[test] + fn test_is_nsfw() { + assert!(!is_nsfw("general")); + assert!(is_nsfw("nsfw")); + assert!(is_nsfw("nsfw-test")); + assert!(!is_nsfw("nsfw-")); + assert!(!is_nsfw("général")); + assert!(is_nsfw("nsfw-général")); + } +} |