From 67d5bc923bcf70c64473c6cf815d51251c8b917f Mon Sep 17 00:00:00 2001 From: Daniel Worrall Date: Mon, 2 Dec 2024 03:21:51 +0000 Subject: [PATCH] feat: add rspec rubocop --- .rubocop.yml | 19 + discordrb.gemspec | 1 + lib/discordrb/events/guilds.rb | 6 + lib/discordrb/events/message.rb | 2 + spec/api/channel_spec.rb | 40 +- spec/api_mock_spec.rb | 3 + spec/bot_spec.rb | 73 ++-- spec/commands/bucket_spec.rb | 114 ++++++ .../command_bot_spec.rb} | 163 ++++---- spec/data/channel_spec.rb | 208 +++++----- spec/data/emoji_spec.rb | 6 +- spec/data/message_spec.rb | 147 +++---- spec/data/role_spec.rb | 23 +- spec/data/webhook_spec.rb | 145 ++++--- spec/errors_spec.rb | 29 +- spec/event_spec.rb | 368 ----------------- spec/events_spec.rb | 375 ++++++++++++++++++ spec/helpers_spec.rb | 30 -- spec/logger_spec.rb | 22 +- spec/main_spec.rb | 37 +- spec/overwrite_spec.rb | 3 + spec/paginator_spec.rb | 13 +- spec/permission_calculator_spec.rb | 71 ++++ spec/permissions_spec.rb | 112 ++---- spec/rate_limiter_spec.rb | 111 ------ spec/rspec_helpers_spec.rb | 32 ++ spec/sodium_spec.rb | 42 -- spec/voice/secret_box_spec.rb | 42 ++ spec/webhooks_spec.rb | 130 +++--- 29 files changed, 1238 insertions(+), 1129 deletions(-) create mode 100644 spec/commands/bucket_spec.rb rename spec/{commands_spec.rb => commands/command_bot_spec.rb} (65%) delete mode 100644 spec/event_spec.rb create mode 100644 spec/events_spec.rb delete mode 100644 spec/helpers_spec.rb create mode 100644 spec/permission_calculator_spec.rb delete mode 100644 spec/rate_limiter_spec.rb create mode 100644 spec/rspec_helpers_spec.rb delete mode 100644 spec/sodium_spec.rb create mode 100644 spec/voice/secret_box_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 368fd948e..e7784cb79 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,7 @@ require: - rubocop-performance - rubocop-rake + - rubocop-rspec inherit_mode: merge: @@ -67,6 +68,24 @@ Style/SingleLineBlockParams: - reduce: [m, e] - inject: [m, e] + +## RSpec Cops + +# TODO: Requires refactoring +RSpec/ExampleLength: + Enabled: false +# Controversial cop, but 15 memoized helpers is a lot and can be a sign of a test that's doing too much +RSpec/MultipleMemoizedHelpers: + Max: 15 +# Don't enforce a discordrb namespace for spec files +RSpec/SpecFilePathFormat: + CustomTransform: + Discordrb: '' +# Stubbing subjects is required for testing methods that call the another method on the same object - essential for unit tests +# We should enable this cop for integration tests if we split them out +RSpec/SubjectStub: + Enabled: false + ################################### ## NEW COPS TO MAKE DECISIONS ON ## ################################### diff --git a/discordrb.gemspec b/discordrb.gemspec index 1da179afa..cbff1d6a2 100644 --- a/discordrb.gemspec +++ b/discordrb.gemspec @@ -46,6 +46,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rubocop', '~> 1.0' spec.add_development_dependency 'rubocop-performance', '~> 1.0' spec.add_development_dependency 'rubocop-rake', '~> 0.6' + spec.add_development_dependency 'rubocop-rspec', '~> 3.0' spec.add_development_dependency 'simplecov', '~> 0.21' spec.add_development_dependency 'yard', '~> 0.9' end diff --git a/lib/discordrb/events/guilds.rb b/lib/discordrb/events/guilds.rb index 6e63aa0ca..b0ffad742 100644 --- a/lib/discordrb/events/guilds.rb +++ b/lib/discordrb/events/guilds.rb @@ -41,6 +41,8 @@ def matches?(event) end ].reduce(true, &:&) end + + alias_method :matching?, :matches? end # Server is created @@ -159,6 +161,8 @@ def matches?(event) matches_all(@attributes[:name], event.emoji.name) { |a, e| a == e } ].reduce(true, &:&) end + + alias_method :matching?, :matches? end # Event handler for {ServerEmojiCreateEvent} @@ -189,5 +193,7 @@ def matches?(event) matches_all(@attributes[:name], event.emoji.name) { |a, e| a == e } ].reduce(true, &:&) end + + alias_method :matching?, :matches? end end diff --git a/lib/discordrb/events/message.rb b/lib/discordrb/events/message.rb index f72446691..d1769e5c3 100644 --- a/lib/discordrb/events/message.rb +++ b/lib/discordrb/events/message.rb @@ -253,6 +253,8 @@ def matches?(event) ].reduce(true, &:&) end + alias_method :matching?, :matches? + # @see EventHandler#after_call def after_call(event) if event.file.nil? diff --git a/spec/api/channel_spec.rb b/spec/api/channel_spec.rb index 56dec2208..1dac2bbb2 100644 --- a/spec/api/channel_spec.rb +++ b/spec/api/channel_spec.rb @@ -3,21 +3,21 @@ require 'discordrb' describe Discordrb::API::Channel do - let(:token) { double('token', to_s: 'bot_token') } + let(:token) { instance_double(String, 'token', to_s: 'bot_token') } let(:channel_id) { instance_double(String, 'channel_id', to_s: 'channel_id') } + let(:message_id) { instance_double(String, 'message_id', to_s: 'message_id') } - describe '.get_reactions' do - let(:message_id) { double('message_id', to_s: 'message_id') } - let(:before_id) { double('before_id', to_s: 'before_id') } - let(:after_id) { double('after_id', to_s: 'after_id') } + before do + allow(Discordrb::API).to receive(:request).with(anything, channel_id, instance_of(Symbol), any_args) + end - before do - allow(Discordrb::API).to receive(:request) - .with(anything, channel_id, :get, kind_of(String), any_args) - end + describe '.get_reactions' do + let(:before_id) { instance_double(String, 'before_id', to_s: 'before_id') } + let(:after_id) { instance_double(String, 'after_id', to_s: 'after_id') } it 'sends requests' do - expect(Discordrb::API).to receive(:request) + described_class.get_reactions(token, channel_id, message_id, 'test', before_id, after_id, 27) + expect(Discordrb::API).to have_received(:request) .with( anything, channel_id, @@ -25,11 +25,11 @@ "#{Discordrb::API.api_base}/channels/#{channel_id}/messages/#{message_id}/reactions/test?limit=27&before=#{before_id}&after=#{after_id}", any_args ) - Discordrb::API::Channel.get_reactions(token, channel_id, message_id, 'test', before_id, after_id, 27) end it 'percent-encodes emoji' do - expect(Discordrb::API).to receive(:request) + described_class.get_reactions(token, channel_id, message_id, "\u{1F44D}", before_id, after_id, 27) + expect(Discordrb::API).to have_received(:request) .with( anything, channel_id, @@ -37,11 +37,11 @@ "#{Discordrb::API.api_base}/channels/#{channel_id}/messages/#{message_id}/reactions/%F0%9F%91%8D?limit=27&before=#{before_id}&after=#{after_id}", any_args ) - Discordrb::API::Channel.get_reactions(token, channel_id, message_id, "\u{1F44D}", before_id, after_id, 27) end it 'uses the maximum limit of 100 if nil is provided' do - expect(Discordrb::API).to receive(:request) + described_class.get_reactions(token, channel_id, message_id, 'test', before_id, after_id, nil) + expect(Discordrb::API).to have_received(:request) .with( anything, channel_id, @@ -49,21 +49,15 @@ "#{Discordrb::API.api_base}/channels/#{channel_id}/messages/#{message_id}/reactions/test?limit=100&before=#{before_id}&after=#{after_id}", any_args ) - Discordrb::API::Channel.get_reactions(token, channel_id, message_id, 'test', before_id, after_id, nil) end end describe '.delete_all_emoji_reactions' do - let(:message_id) { double('message_id', to_s: 'message_id') } let(:emoji) { "\u{1F525}" } - before do - allow(Discordrb::API).to receive(:request) - .with(anything, channel_id, :delete, kind_of(String), any_args) - end - it 'sends requests' do - expect(Discordrb::API).to receive(:request) + described_class.delete_all_emoji_reactions(token, channel_id, message_id, emoji) + expect(Discordrb::API).to have_received(:request) .with( anything, channel_id, @@ -71,8 +65,6 @@ "#{Discordrb::API.api_base}/channels/#{channel_id}/messages/#{message_id}/reactions/#{URI.encode_www_form_component(emoji)}", any_args ) - - Discordrb::API::Channel.delete_all_emoji_reactions(token, channel_id, message_id, emoji) end end end diff --git a/spec/api_mock_spec.rb b/spec/api_mock_spec.rb index 57951c98b..52f99f192 100644 --- a/spec/api_mock_spec.rb +++ b/spec/api_mock_spec.rb @@ -31,6 +31,8 @@ expect(Discordrb::API.last_body).to be_nil end + # TODO: Rework tests to not rely on multiple expectations + # rubocop:disable RSpec/MultipleExpectations it 'parses headers if there is no body' do Discordrb::API.raw_request(:post, ['https://example.com/test', nil, { a: 1, b: 2 }]) @@ -45,4 +47,5 @@ expect(Discordrb::API.last_headers[:a]).to eq 1 expect(Discordrb::API.last_headers[:b]).to eq 2 end + # rubocop:enable RSpec/MultipleExpectations end diff --git a/spec/bot_spec.rb b/spec/bot_spec.rb index cd7b05f7a..a864e5144 100644 --- a/spec/bot_spec.rb +++ b/spec/bot_spec.rb @@ -32,20 +32,29 @@ bot.instance_variable_set(:@servers, server_id => server) end - it 'should set up' do - expect(bot.server(server_id)).to eq(server) - expect(bot.server(server_id).emoji.size).to eq(2) + describe '#initialize' do + it 'sets up servers' do + expect(bot.server(server_id)).to eq(server) + end + + it 'sets up emojis' do + expect(bot.server(server_id).emoji.size).to eq(2) + end end - it 'raises when token string is empty or nil' do - expect { described_class.new(token: '') }.to raise_error('Token string is empty or nil') - expect { described_class.new(token: nil) }.to raise_error('Token string is empty or nil') + shared_examples 'raises when token string is' do |token, value| + it "raises when token string is #{token}" do + expect { described_class.new(token: value) }.to raise_error('Token string is empty or nil') + end end + include_examples 'raises when token string is', 'empty', '' + include_examples 'raises when token string is', 'nil', nil + describe '#parse_mentions' do it 'parses user mentions' do - user_a = double(:user_a) - user_b = double(:user_b) + user_a = instance_double(Discordrb::User, :user_a) + user_b = instance_double(Discordrb::User, :user_b) allow(bot).to receive(:user).with('123').and_return(user_a) allow(bot).to receive(:user).with('456').and_return(user_b) mentions = bot.parse_mentions('<@!123><@!456>', server) @@ -53,8 +62,8 @@ end it 'parses channel mentions' do - channel_a = double(:channel_a) - channel_b = double(:channel_b) + channel_a = instance_double(Discordrb::Channel, :channel_a) + channel_b = instance_double(Discordrb::Channel, :channel_b) allow(bot).to receive(:channel).with('123', server).and_return(channel_a) allow(bot).to receive(:channel).with('456', server).and_return(channel_b) mentions = bot.parse_mentions('<#123><#456>', server) @@ -62,8 +71,8 @@ end it 'parses role mentions' do - role_a = double(:role_a) - role_b = double(:role_b) + role_a = instance_double(Discordrb::Role, :role_a) + role_b = instance_double(Discordrb::Role, :role_b) allow(server).to receive(:role).with('123').and_return(role_a) allow(server).to receive(:role).with('456').and_return(role_b) mentions = bot.parse_mentions('<@&123><@&456>') @@ -71,8 +80,8 @@ end it 'parses emoji mentions' do - emoji_a = double(:emoji_a) - emoji_b = double(:emoji_b) + emoji_a = instance_double(Discordrb::Emoji, :emoji_a) + emoji_b = instance_double(Discordrb::Emoji, :emoji_b) allow(bot).to receive(:emoji).with('123').and_return(emoji_a) allow(bot).to receive(:emoji).with('456').and_return(emoji_b) mentions = bot.parse_mentions('') @@ -99,8 +108,9 @@ describe '#handle_dispatch' do it 'handles GUILD_EMOJIS_UPDATE' do type = :GUILD_EMOJIS_UPDATE - expect(bot).to receive(:raise_event).exactly(4).times + allow(bot).to receive(:raise_event) bot.send(:handle_dispatch, type, dispatch_event) + expect(bot).to have_received(:raise_event).exactly(4).times end context 'when handling a PRESENCE_UPDATE' do @@ -170,18 +180,20 @@ expect(bot).to have_received(:raise_event).with(instance_of(Discordrb::Events::ChannelCreateEvent)) end - it 'does not raise a ChannelCreateEvent if the DM channel is cached' do + it 'doesn\'t raise a ChannelCreateEvent if the DM channel is cached' do allow(channel).to receive(:private?).and_return(true) bot.instance_variable_set(:@pm_channels, { user_id => channel }) bot.send(:handle_dispatch, :MESSAGE_CREATE, message_fixture) - expect(bot).to_not have_received(:raise_event).with(instance_of(Discordrb::Events::ChannelCreateEvent)) + expect(bot).not_to have_received(:raise_event).with(instance_of(Discordrb::Events::ChannelCreateEvent)) end end end describe '#update_guild_emoji' do + # TODO: Rework tests to not rely on multiple expectations + # rubocop:disable RSpec/MultipleExpectations it 'removes an emoji' do bot.send(:update_guild_emoji, dispatch_remove) @@ -217,15 +229,15 @@ expect(emoji.server).to eq(server) expect(emoji.roles).to eq([]) end + # rubocop:enable RSpec/MultipleExpectations end describe '#send_file' do - let(:channel) { double(:channel, resolve_id: double) } + let(:channel) { instance_double(Discordrb::Channel, :channel, resolve_id: double) } it 'defines original_filename when filename is passed' do - original_filename = double(:original_filename) - file = double(:file, original_filename: original_filename, read: true) - new_filename = double('new filename') + file = instance_double(File, :file, read: true) + new_filename = instance_double(String, 'new filename') allow(Discordrb::API::Channel).to receive(:upload_file).and_return('{}') allow(Discordrb::Message).to receive(:new) @@ -234,19 +246,18 @@ expect(file.original_filename).to eq new_filename end - it 'does not define original_filename when filename is nil' do - original_filename = double(:original_filename) - file = double(:file, read: true, original_filename: original_filename) + it 'doesn\'t define original_filename when filename is nil' do + file = instance_double(File, :file, read: true) allow(Discordrb::API::Channel).to receive(:upload_file).and_return('{}') allow(Discordrb::Message).to receive(:new) bot.send_file(channel, file) - expect(file.original_filename).to eq original_filename + expect(file).not_to respond_to(:original_filename) end - it 'prepends "SPOILER_" when spoiler is truthy and the filename does not start with "SPOILER_"' do - file = double(:file, read: true) + it 'prepends "SPOILER_" when spoiler is truthy and the filename doesn\'t start with "SPOILER_"' do + file = instance_double(File, :file, read: true) allow(Discordrb::API::Channel).to receive(:upload_file).and_return('{}') allow(Discordrb::Message).to receive(:new) @@ -255,8 +266,8 @@ expect(file.original_filename).to eq 'SPOILER_file.txt' end - it 'does not prepend "SPOILER_" if the filename starts with "SPOILER_"' do - file = double(:file, read: true, path: 'SPOILER_file.txt') + it 'doesn\'t prepend "SPOILER_" if the filename starts with "SPOILER_"' do + file = instance_double(File, :file, read: true, path: 'SPOILER_file.txt') allow(Discordrb::API::Channel).to receive(:upload_file).and_return('{}') allow(Discordrb::Message).to receive(:new) @@ -266,7 +277,7 @@ end it 'uses the original filename when spoiler is truthy and filename is nil' do - file = double(:file, read: true, path: 'file.txt') + file = instance_double(File, :file, read: true, path: 'file.txt') allow(Discordrb::API::Channel).to receive(:upload_file).and_return('{}') allow(Discordrb::Message).to receive(:new) @@ -278,7 +289,7 @@ describe '#voice_connect' do it 'requires encryption' do - channel = double(:channel, resolve_id: double) + channel = instance_double(Discordrb::Channel, :channel, resolve_id: double) expect { bot.voice_connect(channel, false) }.to raise_error ArgumentError end end diff --git a/spec/commands/bucket_spec.rb b/spec/commands/bucket_spec.rb new file mode 100644 index 000000000..bf5c42319 --- /dev/null +++ b/spec/commands/bucket_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'discordrb' + +# TODO: Rework tests to not rely on multiple expectations +# rubocop:disable RSpec/MultipleExpectations +describe Discordrb::Commands::Bucket do + describe 'rate_limited?' do + shared_examples 'does not rate limit one request' do |bucket| + it 'does not rate limit one request' do + expect(bucket).not_to be_rate_limited(:a) + end + end + + include_examples 'does not rate limit one request', described_class.new(1, 5, 2) + include_examples 'does not rate limit one request', described_class.new(nil, nil, 2) + include_examples 'does not rate limit one request', described_class.new(1, 5, nil) + include_examples 'does not rate limit one request', described_class.new(0, 1, nil) + include_examples 'does not rate limit one request', described_class.new(0, 1_000_000_000, 500_000_000) + + it 'fails to initialize with invalid arguments' do + expect { described_class.new(0, nil, 0) }.to raise_error(ArgumentError) + end + + it 'fails to rate limit something invalid' do + expect { described_class.new(1, 5, 2).rate_limited?("can't RL a string!") }.to raise_error(ArgumentError) + end + + it 'rate limits one request over the limit' do + b = described_class.new(1, 5, nil) + expect(b).not_to be_rate_limited(:a) + expect(b).to be_rate_limited(:a) + end + + it 'rate limits multiple requests that are over the limit' do + b = described_class.new(3, 5, nil) + expect(b).not_to be_rate_limited(:a) + expect(b).not_to be_rate_limited(:a) + expect(b).not_to be_rate_limited(:a) + expect(b).to be_rate_limited(:a) + end + + it 'allows to be passed a custom increment' do + b = described_class.new(5, 5, nil) + expect(b).not_to be_rate_limited(:a, increment: 2) + expect(b).not_to be_rate_limited(:a, increment: 2) + expect(b).to be_rate_limited(:a, increment: 2) + end + + it 'does not rate limit after the limit ran out' do + b = described_class.new(2, 5, nil) + expect(b).not_to be_rate_limited(:a) + expect(b).not_to be_rate_limited(:a) + expect(b).to be_rate_limited(:a) + expect(b).to be_rate_limited(:a, Time.now + 4) + expect(b).not_to be_rate_limited(:a, Time.now + 5) + end + + it 'resets the limit after it runs out' do + b = described_class.new(2, 5, nil) + expect(b).not_to be_rate_limited(:a) + expect(b).not_to be_rate_limited(:a) + expect(b).to be_rate_limited(:a) + expect(b).not_to be_rate_limited(:a, Time.now + 5) + expect(b).not_to be_rate_limited(:a, Time.now + 5.01) + expect(b).to be_rate_limited(:a, Time.now + 5.02) + end + + it 'rates the limit based on the delay' do + b = described_class.new(nil, nil, 2) + expect(b).not_to be_rate_limited(:a) + expect(b).to be_rate_limited(:a) + end + + it 'does not rate limit after the delay runs out' do + b = described_class.new(nil, nil, 2) + expect(b).not_to be_rate_limited(:a) + expect(b).to be_rate_limited(:a) + expect(b).not_to be_rate_limited(:a, Time.now + 2) + expect(b).to be_rate_limited(:a, Time.now + 2) + expect(b).not_to be_rate_limited(:a, Time.now + 4) + expect(b).to be_rate_limited(:a, Time.now + 4) + end + + it 'rate limits based on both the limit and the delay' do + b = described_class.new(2, 5, 2) + expect(b).not_to be_rate_limited(:a) + expect(b).to be_rate_limited(:a) + expect(b).not_to be_rate_limited(:a, Time.now + 2) + expect(b).to be_rate_limited(:a, Time.now + 2) + expect(b).to be_rate_limited(:a, Time.now + 4) + expect(b).not_to be_rate_limited(:a, Time.now + 5) + expect(b).to be_rate_limited(:a, Time.now + 6) + + b = described_class.new(2, 5, 2) + expect(b).not_to be_rate_limited(:a) + expect(b).to be_rate_limited(:a) + expect(b).not_to be_rate_limited(:a, Time.now + 4) + expect(b).to be_rate_limited(:a, Time.now + 4) + expect(b).to be_rate_limited(:a, Time.now + 5) + end + + it 'returns the correct number of times' do + start_time = Time.now + b = described_class.new(2, 5, 2) + expect(b).not_to be_rate_limited(:a, start_time) + expect(b.rate_limited?(:a, start_time).round(2)).to eq(2) + expect(b.rate_limited?(:a, start_time + 1).round(2)).to eq(1) + expect(b).not_to be_rate_limited(:a, start_time + 2.01) + expect(b.rate_limited?(:a, start_time + 2).round(2)).to eq(3) + end + end +end +# rubocop:enable RSpec/MultipleExpectations diff --git a/spec/commands_spec.rb b/spec/commands/command_bot_spec.rb similarity index 65% rename from spec/commands_spec.rb rename to spec/commands/command_bot_spec.rb index d7c288bf1..ac719480d 100644 --- a/spec/commands_spec.rb +++ b/spec/commands/command_bot_spec.rb @@ -12,15 +12,19 @@ '#channel-five' ].freeze +# TODO: Rework tests +# rubocop:disable RSpec/MultipleExpectations +# rubocop:disable Rspec/MultipleMemoizedHelpers +# rubocop:disable RSpec/NestedGroups describe Discordrb::Commands::CommandBot, order: :defined do - let(:server) { double('server', id: 123) } + let(:server) { instance_double(Discordrb::Server, 'server', id: 123) } let(:text_channel_data) { load_data_file(:text_channel) } let(:default_channel_id) { 123 } let(:default_channel_name) { 'test-channel' } let(:user_id) { 321 } let(:user_roles) { [load_data_file(:text_channel), load_data_file(:text_channel)] } - let(:role1) { user_roles[0].tap { |r| r['id'] = 240_172_879_361_212_417 }['id'] } # So we don't have the same ID in both roles. - let(:role2) { user_roles[1]['id'].to_i } + let(:role_one) { user_roles[0].tap { |r| r['id'] = 240_172_879_361_212_417 }['id'] } # So we don't have the same ID in both roles. + let(:role_two) { user_roles[1]['id'].to_i } let(:test_channels) { TEST_CHANNELS } let(:first_channel) { test_channels[0] } let(:second_channel) { test_channels[1] } @@ -28,37 +32,30 @@ let(:fourth_channel) { test_channels[3] } let(:fifth_channel) { test_channels[4] } let(:sixth_channel) do - bot = double('bot') - allow(bot).to receive(:token) { 'fake token' } + bot = instance_double(described_class, 'bot') + allow(bot).to receive(:token).and_return('fake token') Discordrb::Channel.new(text_channel_data, bot, server) end def command_event_double - double('event').tap do |event| - allow(event).to receive :command= + instance_double(Discordrb::Commands::CommandEvent, 'event').tap do |event| + allow(event).to receive_messages(channel: nil, :command= => nil, server: nil) allow(event).to receive(:drain_into) { |e| e } - allow(event).to receive(:server) - allow(event).to receive(:channel) end end def append_author_to_double(event) allow(event).to receive(:author) do - double('member').tap do |member| - allow(member).to receive(:id) { user_id } - allow(member).to receive(:roles) { user_roles } - allow(member).to receive(:permission?) { true } - allow(member).to receive(:webhook?) { false } + instance_double(Discordrb::Member, 'member').tap do |member| + allow(member).to receive_messages(id: user_id, roles: user_roles, permission?: true, webhook?: false) end end end def append_bot_to_double(event) allow(event).to receive(:bot) do - double('bot').tap do |bot| - allow(bot).to receive(:token) { 'fake token' } - allow(bot).to receive(:rate_limited?) { false } - allow(bot).to receive(:attributes) { {} } + instance_double(Discordrb::Commands::CommandBot, 'bot').tap do |bot| + allow(bot).to receive_messages(token: 'fake token', rate_limited?: false, attributes: {}) end end end @@ -87,33 +84,33 @@ def command_event_double_with_channel(channel) end end - context 'no defined commands' do - bot = Discordrb::Commands::CommandBot.new token: 'token', help_available: false + context 'without defined commands' do + bot = described_class.new token: 'token', help_available: false - it 'should successfully trigger the command' do + it 'successfully triggers the command' do event = double - bot.execute_command(:test, event, [], false, false) + expect { bot.execute_command(:test, event, [], false, false) }.not_to raise_error end - it 'should not send anything to the channel' do + it 'doesn\'t send anything to the channel' do event = spy bot.execute_command(:test, event, [], false, false) - expect(spy).to_not have_received :respond + expect(spy).not_to have_received :respond end end - context 'single command' do - bot = Discordrb::Commands::CommandBot.new token: 'token', help_available: false + context 'with a single command' do + bot = described_class.new token: 'token', help_available: false bot.command :name do SIMPLE_RESPONSE end - context 'regular user' do - it 'should return the response' do + context 'with a regular user' do + it 'returns the response' do result = bot.execute_command(:name, command_event_double, [], false, false) expect(result).to eq SIMPLE_RESPONSE @@ -121,74 +118,83 @@ def command_event_double_with_channel(channel) end end - context 'with :command_doesnt_exist_message attribute' do + shared_context 'with :command_doesnt_exist_message attribute' do let(:plain_event) { command_event_double_for_channel(first_channel) } + end - context 'as a string' do - bot = Discordrb::Commands::CommandBot.new(token: 'token', command_doesnt_exist_message: 'command %command% does not exist!') + describe ':command_doesnt_exist_message attribute as a string' do + include_context 'with :command_doesnt_exist_message attribute' - it 'replies with the message including % substitution' do - expect(plain_event).to receive(:respond).with('command bleep_blorp does not exist!') - result = bot.execute_command(:bleep_blorp, plain_event, []) - expect(result).to be_nil - end + bot = described_class.new(token: 'token', command_doesnt_exist_message: 'command %command% does not exist!') + + it 'replies with the message including % substitution' do + allow(plain_event).to receive(:respond) + result = bot.execute_command(:bleep_blorp, plain_event, []) + expect(plain_event).to have_received(:respond).with('command bleep_blorp does not exist!') + expect(result).to be_nil end + end - context 'as a lambda' do - bot = Discordrb::Commands::CommandBot.new(token: 'token', command_doesnt_exist_message: ->(event) { "command %command% does not exist in #{event.channel.name} and 1+2=#{1 + 2}" }) + describe ':command_doesnt_exist_message as a lambda' do + include_context 'with :command_doesnt_exist_message attribute' - it 'executes the lambda and replies with a message including % substitution' do - expect(plain_event).to receive(:respond).with('command bleep_blorp does not exist in test-channel and 1+2=3') - result = bot.execute_command(:bleep_blorp, plain_event, []) - expect(result).to be_nil - end + bot = described_class.new(token: 'token', command_doesnt_exist_message: ->(event) { "command %command% does not exist in #{event.channel.name} and 1+2=#{1 + 2}" }) + + it 'executes the lambda and replies with a message including % substitution' do + allow(plain_event).to receive(:respond) + result = bot.execute_command(:bleep_blorp, plain_event, []) + expect(plain_event).to have_received(:respond).with('command bleep_blorp does not exist in test-channel and 1+2=3') + expect(result).to be_nil end + end - context 'with a nil' do - bot = Discordrb::Commands::CommandBot.new(token: 'token', command_doesnt_exist_message: ->(_event) {}) + describe ':command_doesnt_exist_message as a nil' do + include_context 'with :command_doesnt_exist_message attribute' - it 'does not reply' do - expect(plain_event).to_not receive(:respond) - result = bot.execute_command(:bleep_blorp, plain_event, []) - expect(result).to be_nil - end + bot = described_class.new(token: 'token', command_doesnt_exist_message: ->(_event) {}) + + it 'doesn\'t reply' do + allow(plain_event).to receive(:respond) + result = bot.execute_command(:bleep_blorp, plain_event, []) + expect(plain_event).not_to have_received(:respond) + expect(result).to be_nil end end describe '#execute_command', order: :defined do context 'with role filter', order: :defined do - bot = Discordrb::Commands::CommandBot.new(token: 'token', help_available: false) + bot = described_class.new(token: 'token', help_available: false) describe 'required_roles' do before do # User has both roles. - bot.command :user_has_all, required_roles: [role1, role2] do + bot.command :user_has_all, required_roles: [role_one, role_two] do SIMPLE_RESPONSE end # User has only one of two roles. - bot.command :user_has_one, required_roles: [role1, 123] do + bot.command :user_has_one, required_roles: [role_one, 123] do SIMPLE_RESPONSE end end - it 'responds when the user has all the roles', skip: true do + it 'responds when the user has all the roles', skip: 'Role stubbing is not implemented in tests' do plain_event = command_event_double_for_channel(first_channel) result = bot.execute_command(:user_has_all, plain_event, []) expect(result).to eq SIMPLE_RESPONSE end - it 'does not respond with one role missing', skip: true do + it 'doesn\'t respond with one role missing', skip: 'Role stubbing is not implemented in tests' do plain_event = command_event_double_with_channel(first_channel) result = bot.execute_command(:user_has_one, plain_event, []) - expect(result).to eq nil + expect(result).to be_nil end end describe 'allowed_roles' do before do # User has one role. - bot.command :user_has_one, required_roles: [role1, 123] do + bot.command :user_has_one, required_roles: [role_one, 123] do SIMPLE_RESPONSE end @@ -198,23 +204,23 @@ def command_event_double_with_channel(channel) end end - it 'responds when the user has at least one role', skip: true do + it 'responds when the user has at least one role', skip: 'Role stubbing is not implemented in tests' do plain_event = command_event_double_with_channel(first_channel) result = bot.execute_command(:user_has_one, plain_event, []) expect(result).to eq SIMPLE_RESPONSE end - it 'does not respond to a user with none of the roles' do + it 'doesn\'t respond to a user with none of the roles' do plain_event = command_event_double_with_channel(first_channel) result = bot.execute_command(:any_role, plain_event, []) - expect(result).to eq nil + expect(result).to be_nil end end end context 'with channel filter', order: :defined do context 'when list is not initialized in bot parameters', order: :defined do - bot = Discordrb::Commands::CommandBot.new(token: 'token', help_available: false) + bot = described_class.new(token: 'token', help_available: false) bot.command :name do SIMPLE_RESPONSE @@ -243,11 +249,11 @@ def command_event_double_with_channel(channel) expect(result).to eq SIMPLE_RESPONSE end - it 'does not have channels that were not added' do + it 'doesn\'t have channels that were not added' do expect(bot.attributes[:channels]).not_to include(second_channel, third_channel) end - it 'does not respond in channels not added' do + it 'doesn\'t respond in channels not added' do plain_event2 = command_event_double_for_channel(second_channel) result = bot.execute_command(:name, plain_event2, []) expect(result).to be_nil @@ -259,7 +265,7 @@ def command_event_double_with_channel(channel) end context 'when list is initialized in bot parameters', order: :defined do - bot = Discordrb::Commands::CommandBot.new(token: 'token', help_available: false, channels: [TEST_CHANNELS[0]]) + bot = described_class.new(token: 'token', help_available: false, channels: [TEST_CHANNELS[0]]) bot.command :name do SIMPLE_RESPONSE @@ -275,7 +281,7 @@ def command_event_double_with_channel(channel) expect(result).to eq SIMPLE_RESPONSE end - it 'does not respond in unlisted channels' do + it 'doesn\'t respond in unlisted channels' do event_unlisted = command_event_double_for_channel(second_channel) result = bot.execute_command(:name, event_unlisted, []) expect(result).to be_nil @@ -301,7 +307,7 @@ def command_event_double_with_channel(channel) expect(bot.attributes[:channels]).to contain_exactly(second_channel) end - it 'does not respond in removed channels' do + it 'doesn\'t respond in removed channels' do event_removed = command_event_double_for_channel(first_channel) result = bot.execute_command(:name, event_removed, []) expect(result).to be_nil @@ -318,8 +324,8 @@ def command_event_double_with_channel(channel) end end - context 'listed as a channel name', order: :defined do - bot = Discordrb::Commands::CommandBot.new(token: 'token', help_available: false) + context 'when listed as a channel name', order: :defined do + bot = described_class.new(token: 'token', help_available: false) bot.command :name do SIMPLE_RESPONSE @@ -336,19 +342,19 @@ def command_event_double_with_channel(channel) expect(result).to eq SIMPLE_RESPONSE end - it 'does not modify the channel list while responding to a channel name' do + it 'doesn\'t modify the channel list while responding to a channel name' do expect(bot.attributes[:channels]).to contain_exactly(fourth_channel) end - it 'does not respond for unlisted channels using channel name' do + it 'doesn\'t respond for unlisted channels using channel name' do event = command_event_double_for_channel(name: fifth_channel) result = bot.execute_command(:name, event, []) expect(result).to be_nil end end - context 'listed as an object', order: :defined do - bot = Discordrb::Commands::CommandBot.new(token: 'token', help_available: false) + context 'when listed as an object', order: :defined do + bot = described_class.new(token: 'token', help_available: false) bot.command :name do SIMPLE_RESPONSE @@ -365,19 +371,19 @@ def command_event_double_with_channel(channel) expect(result).to eq SIMPLE_RESPONSE end - it 'does not modify the list while respond to a channel object' do + it 'doesn\'t modify the list while respond to a channel object' do expect(bot.attributes[:channels]).to contain_exactly(sixth_channel) end - it 'does not respond in unlisted channels' do + it 'doesn\'t respond in unlisted channels' do event = command_event_double_for_channel(first_channel) result = bot.execute_command(:name, event, []) expect(result).to be_nil end end - context 'command_bot#channels=', order: :defined do - bot = Discordrb::Commands::CommandBot.new(token: 'token', help_available: false, channels: [TEST_CHANNELS[0], TEST_CHANNELS[1]]) + describe '#channels=', order: :defined do + bot = described_class.new(token: 'token', help_available: false, channels: [TEST_CHANNELS[0], TEST_CHANNELS[1]]) bot.command :name do SIMPLE_RESPONSE @@ -394,7 +400,7 @@ def command_event_double_with_channel(channel) expect(result).to eq SIMPLE_RESPONSE end - it 'does not respond in old channels' do + it 'doesn\'t respond in old channels' do event = command_event_double_for_channel(first_channel) result = bot.execute_command(:name, event, []) expect(result).to be_nil @@ -403,3 +409,6 @@ def command_event_double_with_channel(channel) end end end +# rubocop:enable RSpec/MultipleExpectations +# rubocop:enable Rspec/MultipleMemoizedHelpers +# rubocop:enable RSpec/NestedGroups diff --git a/spec/data/channel_spec.rb b/spec/data/channel_spec.rb index acbae6cda..92837c89d 100644 --- a/spec/data/channel_spec.rb +++ b/spec/data/channel_spec.rb @@ -6,32 +6,32 @@ using APIMock describe Discordrb::Channel do - let(:data) { load_data_file(:text_channel) } - # Instantiate the doubles here so we can apply mocks in the specs - let(:bot) { double('bot') } - let(:server) { double('server', id: double) } - subject(:channel) do - allow(bot).to receive(:token) { 'fake token' } described_class.new(data, bot, server) end + let(:data) { load_data_file(:text_channel) } + # Instantiate the doubles here so we can apply mocks in the specs + let(:bot) { instance_double(Discordrb::Bot, 'bot', token: 'fake token') } + let(:server) { instance_double(Discordrb::Server, 'server', id: double) } + shared_examples 'a Channel property' do |property_name| - it 'should call #update_channel_data with data' do - expect(channel).to receive(:update_channel_data).with(property_name => property_value) + it 'calls #update_channel_data with data' do + allow(channel).to receive(:update_channel_data) channel.__send__("#{property_name}=", property_value) + expect(channel).to have_received(:update_channel_data).with(property_name => property_value) end end describe '#name=' do it_behaves_like 'a Channel property', :name do - let(:property_value) { double('name') } + let(:property_value) { instance_double(String, 'name') } end end describe '#topic=' do it_behaves_like 'a Channel property', :topic do - let(:property_value) { double('topic') } + let(:property_value) { instance_double(String, 'topic') } end end @@ -44,6 +44,7 @@ context 'when toggled from true to false' do subject(:channel) { described_class.new(data.merge('nsfw' => true), double, server) } + it_behaves_like 'a Channel property', :nsfw do let(:property_value) { false } end @@ -53,7 +54,7 @@ describe '#permission_overwrites=' do context 'when permissions_overwrites are explicitly set' do it_behaves_like 'a Channel property', :permission_overwrites do - let(:property_value) { double('permission_overwrites') } + let(:property_value) { instance_double(Array, 'permission_overwrites') } end end end @@ -67,25 +68,32 @@ describe '#slowmode?' do it 'works when the value is 0' do channel.instance_variable_set(:@rate_limit_per_user, 0) - expect(channel.slowmode?).to be_falsy + expect(channel).not_to be_slowmode end it "works when the value isn't 0" do channel.instance_variable_set(:@rate_limit_per_user, 5) - expect(channel.slowmode?).to be_truthy + expect(channel).to be_slowmode end end describe '#update_channel_data' do + let(:channel_data) do + channel_data = instance_double(Hash, 'new data', :[] => double) + allow(channel_data).to receive(:[]).with(:permission_overwrites).and_return(instance_double(Array, 'permission overwrites', map: double)) + channel_data + end + shared_examples('API call') do |property_name, num| - it "should call the API with #{property_name}" do + it "calls the API with #{property_name}" do allow(channel).to receive(:update_data) allow(JSON).to receive(:parse) - data = double(property_name) - expectation = Array.new(num) { anything } << data << any_args - expect(Discordrb::API::Channel).to receive(:update).with(*expectation) - new_data = { property_name => data } + property = double + expectation = Array.new(num) { anything } << property << any_args + allow(Discordrb::API::Channel).to receive(:update) + new_data = { property_name => property } channel.__send__(:update_channel_data, new_data) + expect(Discordrb::API::Channel).to have_received(:update).with(*expectation) end end @@ -97,77 +105,76 @@ include_examples('API call', :parent_id, 9) include_examples('API call', :rate_limit_per_user, 10) - context 'when permission_overwrite are not set' do - it 'should not send permission_overwrite' do + context 'when permission_overwrites are not set' do + it 'doesn\'t send permission_overwrites' do allow(channel).to receive(:update_data) allow(JSON).to receive(:parse) - new_data = double('new data') - allow(new_data).to receive(:[]) + new_data = instance_double(Hash, 'new data', :[] => double) allow(new_data).to receive(:[]).with(:permission_overwrites).and_return(false) - expect(Discordrb::API::Channel).to receive(:update).with(any_args, nil, anything) + allow(Discordrb::API::Channel).to receive(:update) channel.__send__(:update_channel_data, new_data) + expect(Discordrb::API::Channel).to have_received(:update).with(any_args, nil, anything, anything) end end context 'when passed a boolean for nsfw' do - it 'should pass the boolean' do - nsfw = double('nsfw') + it 'passes the boolean' do + nsfw = double channel.instance_variable_set(:@nsfw, nsfw) allow(channel).to receive(:update_data) allow(JSON).to receive(:parse) - new_data = double('new data') - allow(new_data).to receive(:[]) - allow(new_data).to receive(:[]).with(:nsfw).and_return(1) - expect(Discordrb::API::Channel).to receive(:update).with(any_args, nsfw, anything, anything, anything) - channel.__send__(:update_channel_data, new_data) + allow(channel_data).to receive(:[]).with(:nsfw).and_return(1) + allow(Discordrb::API::Channel).to receive(:update) + channel.__send__(:update_channel_data, channel_data) + expect(Discordrb::API::Channel).to have_received(:update).with(any_args, nsfw, anything, anything, anything) end end context 'when passed a non-boolean for nsfw' do - it 'should pass the cached value' do - nsfw = double('nsfw') + it 'passes the cached value' do + nsfw = double channel.instance_variable_set(:@nsfw, nsfw) allow(channel).to receive(:update_data) allow(JSON).to receive(:parse) - new_data = double('new data') - allow(new_data).to receive(:[]) - allow(new_data).to receive(:[]).with(:nsfw).and_return(1) - expect(Discordrb::API::Channel).to receive(:update).with(any_args, nsfw, anything, anything, anything) - channel.__send__(:update_channel_data, new_data) + allow(channel_data).to receive(:[]).with(:nsfw).and_return(1) + allow(Discordrb::API::Channel).to receive(:update) + channel.__send__(:update_channel_data, channel_data) + expect(Discordrb::API::Channel).to have_received(:update).with(any_args, nsfw, anything, anything, anything) end end context 'when passed an Integer for rate_limit_per_user' do - it 'should pass the new value' do + it 'passes the new value' do rate_limit_per_user = 5 channel.instance_variable_set(:@rate_limit_per_user, rate_limit_per_user) allow(channel).to receive(:update_data) allow(JSON).to receive(:parse) - new_data = double('new data') - allow(new_data).to receive(:[]) - allow(new_data).to receive(:[]).with(:rate_limit_per_user).and_return(5) - expect(Discordrb::API::Channel).to receive(:update).with(any_args, rate_limit_per_user) - channel.__send__(:update_channel_data, new_data) + allow(channel_data).to receive(:[]).with(:rate_limit_per_user).and_return(5) + allow(Discordrb::API::Channel).to receive(:update) + channel.__send__(:update_channel_data, channel_data) + expect(Discordrb::API::Channel).to have_received(:update).with(any_args, rate_limit_per_user) end end - it 'should call #update_data with new data' do - response_data = double('new data') - expect(channel).to receive(:update_data).with(response_data) + it 'calls #update_data with new data' do + response_data = double + allow(channel).to receive(:update_data) allow(JSON).to receive(:parse).and_return(response_data) allow(Discordrb::API::Channel).to receive(:update) - channel.__send__(:update_channel_data, double('data', :[] => double('sub_data', map: double))) + channel.__send__(:update_channel_data, channel_data) + expect(channel).to have_received(:update_data).with(response_data) end context 'when NoPermission is raised' do - it 'should not call update_data' do + it 'doesn\'t call update_data' do allow(Discordrb::API::Channel).to receive(:update).and_raise(Discordrb::Errors::NoPermission) - expect(channel).not_to receive(:update_data) + allow(channel).to receive(:update_data) begin - channel.__send__(:update_channel_data, double('data', :[] => double('sub_data', map: double))) + channel.__send__(:update_channel_data, channel_data) rescue Discordrb::Errors::NoPermission nil end + expect(channel).not_to have_received(:update_data) end end end @@ -175,16 +182,17 @@ describe '#update_data' do shared_examples('update property data') do |property_name| context 'when we have new data' do - it 'should assign the property' do - new_data = double('new data', :[] => nil, :key? => true) - test_data = double('test_data') + it 'assigns the property' do + new_data = instance_double(Hash, 'new data', :[] => nil, :key? => true) + test_data = double allow(new_data).to receive(:[]).with(property_name).and_return(test_data) expect { channel.__send__(:update_data, new_data) }.to change { channel.__send__(property_name) }.to test_data end end + context 'when we don\'t have new data' do - it 'should keep the cached value' do - new_data = double('new data', :[] => double('property'), key?: double) + it 'keeps the cached value' do + new_data = instance_double(Hash, 'new data', :[] => double, key?: double) allow(new_data).to receive(:[]).with(property_name).and_return(nil) allow(new_data).to receive(:[]).with(property_name.to_s).and_return(nil) allow(channel).to receive(:process_permission_overwrites) @@ -201,67 +209,71 @@ include_examples('update property data', :nsfw) include_examples('update property data', :parent_id) - it 'should call process_permission_overwrites' do + it 'calls process_permission_overwrites' do allow(Discordrb::API::Channel).to receive(:resolve).and_return('{}') - expect(channel).to receive(:process_permission_overwrites) + allow(channel).to receive(:process_permission_overwrites) channel.__send__(:update_data) + expect(channel).to have_received(:process_permission_overwrites) end context 'when data is not provided' do - it 'should request it from the API' do - expect(Discordrb::API::Channel).to receive(:resolve).and_return('{}') + it 'requests it from the API' do + allow(Discordrb::API::Channel).to receive(:resolve).and_return('{}') channel.__send__(:update_data) + expect(Discordrb::API::Channel).to have_received(:resolve) end end end describe '#delete_messages' do - it 'should fail with more than 100 messages' do + it 'fails with more than 100 messages' do messages = [*1..101] expect { channel.delete_messages(messages) }.to raise_error(ArgumentError) end - it 'should fail with less than 2 messages' do + it 'fails with less than 2 messages' do messages = [1] expect { channel.delete_messages(messages) }.to raise_error(ArgumentError) end - it 'should resolve message ids' do - message = double('message', resolve_id: double) + it 'resolves message ids' do + message = instance_double(Discordrb::Message, 'message', resolve_id: double) num = 3 messages = Array.new(num) { message } << 0 allow(channel).to receive(:bulk_delete) - expect(message).to receive(:resolve_id).exactly(num).times channel.delete_messages(messages) + expect(message).to have_received(:resolve_id).exactly(num).times end - it 'should call #bulk_delete' do + it 'calls #bulk_delete' do messages = [1, 2, 3] - expect(channel).to receive(:bulk_delete) + allow(channel).to receive(:bulk_delete) channel.delete_messages(messages) + expect(channel).to have_received(:bulk_delete) end end describe '#bulk_delete' do - it 'should log with old messages' do + it 'logs with old messages' do messages = [1, 2, 3, 4] allow(Discordrb::IDObject).to receive(:synthesise).and_return(3) allow(Discordrb::API::Channel).to receive(:bulk_delete_messages) - expect(Discordrb::LOGGER).to receive(:warn).exactly(2).times + allow(Discordrb::LOGGER).to receive(:warn) channel.__send__(:bulk_delete, messages) + expect(Discordrb::LOGGER).to have_received(:warn).twice end context 'when in strict mode' do - it 'should raise ArgumentError with old messages' do + it 'raises ArgumentError with old messages' do messages = [1, 2, 3] expect { channel.__send__(:bulk_delete, messages, true) }.to raise_error(ArgumentError) end end context 'when in non-strict mode' do - let(:@bot) { double('bot', token: 'token') } + let(:@bot) { instance_double(Discordrb::Bot, 'bot', token: 'token') } - it 'should remove old messages ' do + it 'removes old messages' do allow(Discordrb::IDObject).to receive(:synthesise).and_return(4) messages = [1, 2, 3, 4] @@ -276,8 +288,8 @@ end describe '#process_permission_overwrites' do - it 'should assign permission overwrites' do - overwrite = double('overwrite') + it 'assigns permission overwrites' do + overwrite = instance_double(Discordrb::Overwrite, 'overwrite') element = { 'id' => 1 } overwrites = [element] allow(Discordrb::Overwrite).to receive(:from_hash).and_call_original @@ -288,67 +300,69 @@ end describe '#sort_after' do - it 'should call the API' do - allow(server).to receive(:channels).and_return([]) - allow(server).to receive(:id).and_return(double) - expect(Discordrb::API::Server).to receive(:update_channel_positions) + it 'calls the API' do + allow(server).to receive_messages(channels: [], id: double) + allow(Discordrb::API::Server).to receive(:update_channel_positions) channel.sort_after + expect(Discordrb::API::Server).to have_received(:update_channel_positions) end - it 'should only send channels of its own type' do - channels = Array.new(10) { |i| double("channel #{i}", type: i % 4, parent_id: nil, position: i, id: i) } - allow(server).to receive(:channels).and_return(channels) - allow(server).to receive(:id).and_return(double) + it 'only sends channels of its own type' do + channels = Array.new(10) { |i| instance_double(described_class, "channel #{i}", type: i % 4, parent_id: nil, position: i, id: i) } + allow(server).to receive_messages(channels: channels, id: double) non_text_channels = channels.reject { |e| e.type == 0 } - expect(Discordrb::API::Server).to receive(:update_channel_positions) - .with(any_args, an_array_excluding(*non_text_channels.map { |e| { id: e.id, position: instance_of(Integer) } })) + allow(Discordrb::API::Server).to receive(:update_channel_positions) channel.sort_after + expect(Discordrb::API::Server).to have_received(:update_channel_positions) + .with(any_args, an_array_excluding(*non_text_channels.map { |e| { id: e.id, position: instance_of(Integer) } })) end context 'when other is not on this server' do - it 'should raise ArgumentError' do - other = double('other', server: double('other server'), resolve_id: double, category?: nil, type: channel.type) + it 'raises ArgumentError' do + other = instance_double(described_class, 'other channel', server: instance_double(Discordrb::Server, 'other server'), resolve_id: double, category?: nil, type: channel.type) allow(bot).to receive(:channel).and_return(other) expect { channel.sort_after(other) }.to raise_error(ArgumentError) end end context 'when other is not of Channel, NilClass, #resolve_id' do - it 'should raise TypeError' do + it 'raises TypeError' do expect { channel.sort_after(double) }.to raise_error(TypeError) end end context 'when other channel is not the same type' do - it 'should raise ArgumentError' do - other_channel = double('other', resolve_id: double, type: double, category?: nil) + it 'raises ArgumentError' do + other_channel = instance_double(described_class, 'other channel', resolve_id: double, type: double, category?: nil) allow(bot).to receive(:channel).and_return(other_channel) expect { channel.sort_after(other_channel) }.to raise_error(ArgumentError) end end context 'when channel is in a category' do - it 'should send parent_id' do - category = double('category', id: 1) - other_channel = double('other', id: 2, resolve_id: double, type: channel.type, category?: nil, server: channel.server, parent: category, position: 5) + it 'sends parent_id' do + category = instance_double(described_class, 'category', id: 1) + other_channel = instance_double(described_class, 'other channel', id: 2, resolve_id: double, type: channel.type, category?: nil, server: channel.server, parent: category, position: 5) allow(category).to receive(:children).and_return [other_channel, channel] allow(bot).to receive(:channel).and_return(other_channel) - expect(Discordrb::API::Server).to receive(:update_channel_positions) - .with(any_args, [{ id: 2, position: 0 }, { id: channel.id, position: 1, parent_id: category.id }]) + allow(Discordrb::API::Server).to receive(:update_channel_positions) channel.sort_after(other_channel) + expect(Discordrb::API::Server).to have_received(:update_channel_positions) + .with(any_args, [{ id: 2, position: 0 }, { id: channel.id, position: 1, parent_id: category.id }]) end end context 'when channel is not in a category' do - it 'should send null' do - other_channel = double('other', id: 2, resolve_id: double, type: channel.type, category?: nil, server: channel.server, parent: nil, parent_id: nil, position: 5) + it 'sends null' do + other_channel = instance_double(described_class, 'other channel', id: 2, resolve_id: double, type: channel.type, category?: nil, server: channel.server, parent: nil, parent_id: nil, position: 5) allow(server).to receive(:channels).and_return [other_channel, channel] allow(bot).to receive(:channel).and_return(other_channel) - expect(Discordrb::API::Server).to receive(:update_channel_positions) - .with(any_args, [{ id: 2, position: 0 }, { id: channel.id, position: 1, parent_id: nil }]) + allow(Discordrb::API::Server).to receive(:update_channel_positions) channel.sort_after(other_channel) + expect(Discordrb::API::Server).to have_received(:update_channel_positions) + .with(any_args, [{ id: 2, position: 0 }, { id: channel.id, position: 1, parent_id: nil }]) end end end diff --git a/spec/data/emoji_spec.rb b/spec/data/emoji_spec.rb index 9803a9a6d..8d1f06355 100644 --- a/spec/data/emoji_spec.rb +++ b/spec/data/emoji_spec.rb @@ -3,14 +3,14 @@ require 'discordrb' describe Discordrb::Emoji do - let(:bot) { double('bot') } - subject(:emoji) do - server = double('server', role: double) + server = instance_double(Discordrb::Server, 'server', role: double) described_class.new(emoji_data, bot, server) end + let(:bot) { instance_double(Discordrb::Bot, 'bot') } + fixture :emoji_data, %i[emoji] describe '#mention' do diff --git a/spec/data/message_spec.rb b/spec/data/message_spec.rb index 86dd4b2b5..15c892a03 100644 --- a/spec/data/message_spec.rb +++ b/spec/data/message_spec.rb @@ -3,31 +3,24 @@ require 'discordrb' describe Discordrb::Message do - let(:server) { double('server') } - let(:channel) { double('channel', server: server) } - let(:token) { double('token') } - let(:bot) { double('bot', channel: channel, token: token) } - let(:server_id) { instance_double('String', 'server_id') } - let(:channel_id) { instance_double('String', 'channel_id') } - let(:message_id) { instance_double('String', 'message_id') } + let(:server) { instance_double(Discordrb::Server, 'server') } + let(:channel) { instance_double(Discordrb::Channel, 'channel', server: server) } + let(:token) { instance_double(String, 'token') } + let(:bot) { instance_double(Discordrb::Bot, 'bot', channel: channel, token: token) } + let(:server_id) { instance_double(String, 'server_id') } + let(:channel_id) { instance_double(String, 'channel_id') } + let(:message_id) { instance_double(String, 'message_id') } before do - allow(server_id).to receive(:to_i).and_return(server_id) - allow(channel_id).to receive(:to_i).and_return(channel_id) - allow(message_id).to receive(:to_i).and_return(message_id) + allow(message_id).to receive_messages(to_i: message_id, to_s: 'message_id') + allow(server_id).to receive_messages(to_i: server_id, to_s: 'server_id') + allow(channel_id).to receive_messages(to_i: channel_id, to_s: 'channel_id') - allow(message_id).to receive(:to_s).and_return('message_id') - allow(server_id).to receive(:to_s).and_return('server_id') - allow(channel_id).to receive(:to_s).and_return('channel_id') - - allow(server).to receive(:id).and_return(server_id) - allow(channel).to receive(:id).and_return(channel_id) + allow(server).to receive_messages(id: server_id, member: nil) + allow(channel).to receive_messages(id: channel_id, private?: nil, text?: nil) allow(bot).to receive(:server).with(server_id).and_return(server) allow(bot).to receive(:channel).with(channel_id).and_return(channel) - allow(server).to receive(:member) - allow(channel).to receive(:private?) - allow(channel).to receive(:text?) allow(bot).to receive(:ensure_user).with message_author end @@ -43,8 +36,9 @@ # Bot will receive #ensure_user because the observed message author # is not present in the server cache, which is possible # (for example) if the author had left the server. - expect(bot).to receive(:ensure_user).with message_author + allow(bot).to receive(:ensure_user) described_class.new(message_data, bot) + expect(bot).to have_received(:ensure_user).with message_author end end @@ -82,6 +76,7 @@ message = described_class.new(data, bot) message.emoji message.emoji + expect(bot).to have_received(:parse_mentions).once end end @@ -111,12 +106,14 @@ describe '#reacted_with' do let(:message) { described_class.new(message_data, bot) } - let(:emoji) { double('emoji') } - let(:reaction) { double('reaction') } + let(:emoji) { instance_double(Discordrb::Emoji, 'emoji') } + let(:reaction) { instance_double(Discordrb::Reaction, 'reaction') } fixture :user_data, %i[user] before do + allow(Discordrb::API::Channel).to receive(:get_reactions).and_return([].to_json) + # Return the appropriate number of users based on after_id allow(Discordrb::API::Channel).to receive(:get_reactions) .with(any_args, nil, anything) # ..., after_id, limit @@ -128,96 +125,99 @@ end it 'calls the API method' do - expect(Discordrb::API::Channel).to receive(:get_reactions) - .with(any_args, '\u{1F44D}', nil, nil, 27) - message.reacted_with('\u{1F44D}', limit: 27) + expect(Discordrb::API::Channel).to have_received(:get_reactions) + .with(any_args, '\u{1F44D}', nil, nil, 27) end it 'fetches all users when limit is nil' do - expect(Discordrb::Paginator).to receive(:new).with(nil, :down) + allow(Discordrb::Paginator).to receive(:new).with(nil, :down) message.reacted_with('\u{1F44D}', limit: nil) + expect(Discordrb::Paginator).to have_received(:new).with(nil, :down) end it 'converts Emoji to strings' do - allow(emoji).to receive(:to_reaction).and_return('123') - - expect(Discordrb::API::Channel).to receive(:get_reactions) - .with(any_args, '123', nil, nil, anything) + string = instance_double(String, 'string') + allow(emoji).to receive(:to_reaction).and_return(instance_double(Discordrb::Reaction, 'reaction', to_s: string)) message.reacted_with(emoji) + expect(Discordrb::API::Channel).to have_received(:get_reactions) + .with(any_args, string, nil, nil, anything) end it 'converts Reaction to strings' do - allow(reaction).to receive(:to_s).and_return('123') - - expect(Discordrb::API::Channel).to receive(:get_reactions) - .with(any_args, '123', nil, nil, anything) + reaction_string = instance_double(String, 'reaction string') + allow(reaction).to receive(:to_s).and_return(reaction_string) message.reacted_with(reaction) + expect(Discordrb::API::Channel).to have_received(:get_reactions) + .with(any_args, reaction_string, nil, nil, anything) end end describe '#all_reaction_users' do let(:message) { described_class.new(message_data, bot) } - let(:reaction1) { double('reaction') } - let(:reaction2) { double('reaction') } - let(:user1) { double('user') } - let(:user2) { double('user') } - let(:user3) { double('user') } + let(:reaction_one) { instance_double(Discordrb::Reaction, 'reaction 1') } + let(:reaction_two) { instance_double(Discordrb::Reaction, 'reaction 2') } + let(:user_one) { instance_double(Discordrb::User, 'user 1') } + let(:user_two) { instance_double(Discordrb::User, 'user 2') } + let(:user_three) { instance_double(Discordrb::User, 'user 3') } before do - message.instance_variable_set(:@reactions, [reaction1, reaction2]) - allow(reaction1).to receive(:to_s).and_return('123') - allow(reaction2).to receive(:to_s).and_return('456') + message.instance_variable_set(:@reactions, [reaction_one, reaction_two]) + allow(reaction_one).to receive(:to_s).and_return('123') + allow(reaction_two).to receive(:to_s).and_return('456') allow(message).to receive(:reacted_with) - .with(reaction1, limit: 100) - .and_return([user1, user2]) + .with(reaction_one, limit: 100) + .and_return([user_one, user_two]) allow(message).to receive(:reacted_with) - .with(reaction2, limit: 100) - .and_return([user1, user3]) + .with(reaction_two, limit: 100) + .and_return([user_one, user_three]) end it 'returns a filled hash' do reactions_hash = message.all_reaction_users - expect(reactions_hash).to eq({ '123' => [user1, user2], '456' => [user1, user3] }) + expect(reactions_hash).to eq({ '123' => [user_one, user_two], '456' => [user_one, user_three] }) end end describe '#reply!' do let(:message) { described_class.new(message_data, bot) } - let(:content) { instance_double('String', 'content') } - let(:mention) { instance_double('TrueClass', 'mention') } + let(:content) { instance_double(String, 'content') } + let(:mention) { instance_double(TrueClass, 'mention') } it 'responds with a message_reference' do - expect(message).to receive(:respond).with(content, false, nil, nil, hash_including(:replied_user), message, nil) - + allow(message).to receive(:respond) message.reply!(content) + + expect(message).to have_received(:respond).with(content, false, nil, nil, hash_including(:replied_user), message, nil) end it 'sets replied_user in allowed_mentions' do - expect(message).to receive(:respond).with(content, false, nil, nil, { replied_user: mention }, message, nil) - + allow(message).to receive(:respond) message.reply!(content, mention_user: mention) + + expect(message).to have_received(:respond).with(content, false, nil, nil, { replied_user: mention }, message, nil) end context 'when allowed_mentions is false' do - let(:mention) { double('mention') } + let(:mention) { instance_double(TrueClass, 'mention') } it 'sets parse to an empty array add merges the mention_user param' do - expect(message).to receive(:respond).with(content, false, nil, nil, { parse: [], replied_user: mention }, message, nil) - + allow(message).to receive(:respond) message.reply!(content, allowed_mentions: false, mention_user: mention) + + expect(message).to have_received(:respond).with(content, false, nil, nil, { parse: [], replied_user: mention }, message, nil) end end context 'when allowed_mentions is an AllowedMentions' do - let(:hash) { instance_double('Hash', 'hash') } - let(:allowed_mentions) { instance_double('Discordrb::AllowedMentions', 'allowed_mentions') } - let(:mention_user) { instance_double('TrueClass', 'mention_user') } + let(:hash) { instance_double(Hash, 'hash') } + let(:allowed_mentions) { instance_double(Discordrb::AllowedMentions, 'allowed_mentions') } + let(:mention_user) { instance_double(TrueClass, 'mention_user') } before do allow(allowed_mentions).to receive(:to_hash).and_return(hash) @@ -226,37 +226,40 @@ end it 'converts it to a hash to set the replied_user key' do - expect(message).to receive(:respond).with(content, false, nil, nil, hash, message, nil) + allow(message).to receive(:respond) message.reply!(content, allowed_mentions: allowed_mentions, mention_user: mention_user) + expect(message).to have_received(:respond).with(content, false, nil, nil, hash, message, nil) end end end describe '#reply' do let(:message) { described_class.new(message_data, bot) } - let(:content) { instance_double('String', 'content') } + let(:content) { instance_double(String, 'content') } it 'sends a message to a channel' do - expect(channel).to receive(:send_message).with(content) - + allow(channel).to receive(:send_message) message.reply(content) + + expect(channel).to have_received(:send_message).with(content) end end describe '#respond' do let(:message) { described_class.new(message_data, bot) } - let(:content) { instance_double('String', 'content') } - let(:tts) { instance_double('TrueClass', 'tts') } - let(:embed) { instance_double('Discordrb::Webhooks::Embed', 'embed') } - let(:attachments) { instance_double('Array', 'attachments') } - let(:allowed_mentions) { instance_double('Hash', 'allowed_mentions') } - let(:message_reference) { instance_double('Discordrb::Message') } - let(:components) { instance_double('Discordrb::Webhooks::View') } + let(:content) { instance_double(String, 'content') } + let(:tts) { instance_double(TrueClass, 'tts') } + let(:embed) { instance_double(Discordrb::Webhooks::Embed, 'embed') } + let(:attachments) { instance_double(Array, 'attachments') } + let(:allowed_mentions) { instance_double(Hash, 'allowed_mentions') } + let(:message_reference) { instance_double(described_class) } + let(:components) { instance_double(Discordrb::Webhooks::View) } it 'forwards arguments to Channel#send_message' do - expect(channel).to receive(:send_message).with(content, tts, embed, attachments, allowed_mentions, message_reference, components) - + allow(channel).to receive(:send_message) message.respond(content, tts, embed, attachments, allowed_mentions, message_reference, components) + + expect(channel).to have_received(:send_message).with(content, tts, embed, attachments, allowed_mentions, message_reference, components) end end end diff --git a/spec/data/role_spec.rb b/spec/data/role_spec.rb index 9f7c4562d..9adb90f55 100644 --- a/spec/data/role_spec.rb +++ b/spec/data/role_spec.rb @@ -3,13 +3,13 @@ require 'discordrb' describe Discordrb::Role do - let(:server) { double('server', id: double) } - let(:bot) { double('bot', token: double) } - subject(:role) do described_class.new(role_data, bot, server) end + let(:server) { instance_double(Discordrb::Server, 'server', id: double) } + let(:bot) { instance_double(Discordrb::Bot, 'bot', token: double) } + fixture :role_data, %i[role] describe '#sort_above' do @@ -17,8 +17,8 @@ it 'sorts the role to position 1' do allow(server).to receive(:update_role_positions) allow(server).to receive(:roles).and_return [ - double(id: 0, position: 0), - double(id: 1, position: 1) + instance_double(described_class, id: 0, position: 0), + instance_double(described_class, id: 1, position: 1) ] new_position = role.sort_above @@ -28,14 +28,13 @@ context 'when other is given' do it 'sorts above other' do - other = double(id: 1, position: 1, resolve_id: 1) + other = instance_double(described_class, id: 1, position: 1, resolve_id: 1) allow(server).to receive(:update_role_positions) - allow(server).to receive(:role).and_return other - allow(server).to receive(:roles).and_return [ - double(id: 0, position: 0), - other, - double(id: 2, position: 2) - ] + allow(server).to receive_messages(role: other, roles: [ + instance_double(described_class, id: 0, position: 0), + other, + instance_double(described_class, id: 2, position: 2) + ]) new_position = role.sort_above(other) expect(new_position).to eq 2 diff --git a/spec/data/webhook_spec.rb b/spec/data/webhook_spec.rb index d6c5a5a05..6b5963f20 100644 --- a/spec/data/webhook_spec.rb +++ b/spec/data/webhook_spec.rb @@ -3,16 +3,16 @@ require 'discordrb' describe Discordrb::Webhook do - let(:token) { double('token') } - let(:reason) { double('reason') } - let(:server) { double('server', member: double) } - let(:channel) { double('channel', server: server) } - let(:bot) { double('bot', channel: channel, token: token) } - subject(:webhook) do described_class.new(webhook_data, bot) end + let(:token) { double } + let(:reason) { double } + let(:server) { instance_double(Discordrb::Server, 'server', member: double) } + let(:channel) { instance_double(Discordrb::Channel, 'channel', server: server) } + let(:bot) { instance_double(Discordrb::Bot, 'bot', channel: channel, token: token) } + fixture :webhook_data, %i[webhook] fixture_property :webhook_name, :webhook_data, ['name'] fixture_property :webhook_channel_id, :webhook_data, ['channel_id'], :to_i @@ -33,84 +33,95 @@ fixture_property :avatar_string, :avatar_data, ['avatar'] describe '#initialize' do - it 'sets readers' do - expect(webhook.name).to eq webhook_name - expect(webhook.id).to eq webhook_id - expect(webhook.token).to eq webhook_token - expect(webhook.avatar).to eq webhook_avatar - expect(webhook.server).to eq server - expect(webhook.channel).to eq channel + shared_examples 'sets reader' do |property, reference| + it "sets #{property}" do + # we pass a symbol to send to avoid calling the method when including the shared example - we can only call it in the test + expect(webhook.send(property)).to eq send(reference) + end end + include_examples 'sets reader', :name, :webhook_name + include_examples 'sets reader', :id, :webhook_id + include_examples 'sets reader', :token, :webhook_token + include_examples 'sets reader', :avatar, :webhook_avatar + include_examples 'sets reader', :server, :server + include_examples 'sets reader', :channel, :channel + + # TODO: Rework redundant test with better setup context 'when webhook from a token' do before { webhook.instance_variable_set(:@owner, nil) } + it 'doesn\'t set owner' do - expect(webhook.owner).to eq nil + expect(webhook.owner).to be_nil end end - context 'when webhook is from auth' do - context 'when owner cached' do - let(:member) { double('member') } - let(:server) { double('server', member: member) } + context 'when owner cached' do + let(:member) { double } + let(:server) { instance_double(Discordrb::Server, 'server', member: member) } - it 'sets owner from cache' do - expect(webhook.owner).to eq member - end + it 'sets owner from cache' do + expect(webhook.owner).to eq member end + end - context 'when owner not cached' do - let(:server) { double('server', member: nil) } - let(:user) { double('user') } - let(:bot) { double('bot', channel: channel, ensure_user: user) } + context 'when owner not cached' do + let(:server) { instance_double(Discordrb::Server, 'server', member: nil) } + let(:user) { double } + let(:bot) { instance_double(Discordrb::Bot, 'bot', channel: channel, ensure_user: user) } - it 'gets user' do - expect(webhook.owner).to eq user - end + it 'gets user' do + expect(webhook.owner).to eq user end end end describe '#avatar=' do it 'calls update_webhook' do - expect(webhook).to receive(:update_webhook).with(avatar: avatar_string) + allow(webhook).to receive(:update_webhook) webhook.avatar = avatar_string + expect(webhook).to have_received(:update_webhook).with(avatar: avatar_string) end end describe '#delete_avatar' do it 'calls update_webhook' do - expect(webhook).to receive(:update_webhook).with(avatar: nil) + allow(webhook).to receive(:update_webhook) webhook.delete_avatar + expect(webhook).to have_received(:update_webhook).with(avatar: nil) end end describe '#channel=' do it 'calls update_webhook' do - expect(webhook).to receive(:update_webhook).with(channel_id: edited_webhook_channel_id.to_i) + allow(webhook).to receive(:update_webhook) webhook.channel = edited_webhook_channel_id + expect(webhook).to have_received(:update_webhook).with(channel_id: edited_webhook_channel_id.to_i) end end describe '#name=' do it 'calls update_webhook' do - expect(webhook).to receive(:update_webhook).with(name: edited_webhook_name) + allow(webhook).to receive(:update_webhook) webhook.name = edited_webhook_name + expect(webhook).to have_received(:update_webhook).with(name: edited_webhook_name) end end describe '#update' do it 'calls update_webhook' do - expect(webhook).to receive(:update_webhook).with(avatar: avatar_string, channel_id: edited_webhook_channel_id.to_i, name: edited_webhook_name, reason: reason) + allow(webhook).to receive(:update_webhook) webhook.update(avatar: avatar_string, channel: edited_webhook_channel_id, name: edited_webhook_name, reason: reason) + expect(webhook).to have_received(:update_webhook).with(avatar: avatar_string, channel_id: edited_webhook_channel_id.to_i, name: edited_webhook_name, reason: reason) end end describe '#delete' do context 'when webhook is from auth' do it 'calls the API' do - expect(Discordrb::API::Webhook).to receive(:delete_webhook).with(token, webhook_id, reason) + allow(Discordrb::API::Webhook).to receive(:delete_webhook) webhook.delete(reason) + expect(Discordrb::API::Webhook).to have_received(:delete_webhook).with(token, webhook_id, reason) end end @@ -118,26 +129,29 @@ before { webhook.instance_variable_set(:@owner, nil) } it 'calls the token API' do - expect(Discordrb::API::Webhook).to receive(:token_delete_webhook).with(webhook_token, webhook_id, reason) + allow(Discordrb::API::Webhook).to receive(:token_delete_webhook) webhook.delete(reason) + expect(Discordrb::API::Webhook).to have_received(:token_delete_webhook).with(webhook_token, webhook_id, reason) end end end describe '#avatar_url' do - context 'avatar is set' do + context 'when avatar is set' do it 'calls the correct API helper' do - expect(Discordrb::API::User).to receive(:avatar_url).with(webhook_id, webhook_avatar) + allow(Discordrb::API::User).to receive(:avatar_url) webhook.avatar_url + expect(Discordrb::API::User).to have_received(:avatar_url).with(webhook_id, webhook_avatar) end end - context 'avatar is not set' do + context 'when avatar is not set' do before { webhook.instance_variable_set(:@avatar, nil) } it 'calls the correct API helper' do - expect(Discordrb::API::User).to receive(:default_avatar) + allow(Discordrb::API::User).to receive(:default_avatar) webhook.avatar_url + expect(Discordrb::API::User).to have_received(:default_avatar) end end end @@ -151,29 +165,30 @@ describe '#token?' do context 'when webhook is from auth' do it 'returns false' do - expect(webhook.token?).to eq false + expect(webhook.token?).to be false end end context 'when webhook is from token' do before { webhook.instance_variable_set(:@owner, nil) } + it 'returns true' do - expect(webhook.token?).to eq true + expect(webhook.token?).to be true end end end describe '#avatarise' do - context 'avatar responds to read' do + context 'when avatar responds to read' do it 'returns encoded' do - avatar = double('avatar', read: 'text') + avatar = instance_double(IO, 'avatar', read: 'text') expect(webhook.send(:avatarise, avatar)).to eq "data:image/jpg;base64,#{Base64.strict_encode64('text')}" end end - context 'avatar does not respond to read' do + context 'when avatar does not respond to read' do it 'returns itself' do - avatar = double('avatar') + avatar = double expect(webhook.send(:avatarise, avatar)).to eq avatar end end @@ -181,20 +196,20 @@ describe '#update_internal' do it 'sets name' do - name = double('name') + name = double webhook.send(:update_internal, 'name' => name) expect(webhook.instance_variable_get(:@name)).to eq name end it 'sets avatar' do - avatar = double('avatar') + avatar = double webhook.send(:update_internal, 'avatar' => avatar) expect(webhook.instance_variable_get(:@avatar_id)).to eq avatar end it 'sets channel' do - channel = double('channel') - channel_id = double('channel_id') + channel = double + channel_id = double allow(bot).to receive(:channel).with(channel_id).and_return(channel) webhook.send(:update_internal, 'channel_id' => channel_id) expect(webhook.instance_variable_get(:@channel)).to eq channel @@ -202,35 +217,38 @@ end describe '#update_webhook' do - context 'API returns valid data' do + context 'when the API returns valid data' do it 'calls update_internal' do webhook - data = double('data', :[] => double) + data = instance_double(Hash, 'data', :[] => double) allow(JSON).to receive(:parse).and_return(data) allow(Discordrb::API::Webhook).to receive(:update_webhook) - expect(webhook).to receive(:update_internal).with(data) - webhook.send(:update_webhook, double('data', delete: reason)) + allow(webhook).to receive(:update_internal) + webhook.send(:update_webhook, instance_double(Hash, 'data', delete: reason)) + expect(webhook).to have_received(:update_internal).with(data) end end - context 'API returns error' do + context 'when the API returns an error' do it 'doesn\'t call update_internal' do webhook - data = double('data', :[] => nil) + data = instance_double(Hash, 'data', :[] => nil) allow(JSON).to receive(:parse).and_return(data) allow(Discordrb::API::Webhook).to receive(:update_webhook) - expect(webhook).to_not receive(:update_internal) - webhook.send(:update_webhook, double('data', delete: reason)) + allow(webhook).to receive(:update_internal) + webhook.send(:update_webhook, instance_double(Hash, 'data', delete: reason)) + expect(webhook).not_to have_received(:update_internal) end end context 'when webhook is from auth' do it 'calls auth API' do webhook - data = double('data', delete: reason) - allow(JSON).to receive(:parse).and_return(double('received_data', :[] => double)) - expect(Discordrb::API::Webhook).to receive(:update_webhook).with(token, webhook_id, data, reason) + data = instance_double(Hash, 'data', delete: reason) + allow(JSON).to receive(:parse).and_return(instance_double(Hash, 'received_data', :[] => double)) + allow(Discordrb::API::Webhook).to receive(:update_webhook) webhook.send(:update_webhook, data) + expect(Discordrb::API::Webhook).to have_received(:update_webhook).with(token, webhook_id, data, reason) end end @@ -238,10 +256,11 @@ before { webhook.instance_variable_set(:@owner, nil) } it 'calls token API' do - data = double('data', delete: reason) - allow(JSON).to receive(:parse).and_return(double('received_data', :[] => double)) - expect(Discordrb::API::Webhook).to receive(:token_update_webhook).with(webhook_token, webhook_id, data, reason) + data = instance_double(Hash, 'data', delete: reason) + allow(JSON).to receive(:parse).and_return(instance_double(Hash, 'received_data', :[] => double)) + allow(Discordrb::API::Webhook).to receive(:token_update_webhook) webhook.send(:update_webhook, data) + expect(Discordrb::API::Webhook).to have_received(:token_update_webhook).with(webhook_token, webhook_id, data, reason) end end end diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index f3ed90ba3..5cd3d784f 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -3,40 +3,43 @@ require 'discordrb' describe Discordrb::Errors do - describe 'Code' do - it 'should create a class without errors' do - Discordrb::Errors.Code(10_000) + describe '.Code' do + it 'creates a class without errors' do + expect { described_class.Code(10_000) }.not_to raise_error end describe 'the created class' do - it 'should contain the correct code' do - classy = Discordrb::Errors.Code(10_001) + it 'contains the correct code' do + classy = described_class.Code(10_001) expect(classy.code).to eq(10_001) end - it 'should create an instance with the correct code' do - classy = Discordrb::Errors.Code(10_002) + # TODO: Rework test to not rely on multiple expectations + # rubocop:disable RSpec/MultipleExpectations + it 'creates an instance with the correct code' do + classy = described_class.Code(10_002) error = classy.new 'random message' expect(error.code).to eq(10_002) expect(error.message).to eq 'random message' end + # rubocop:enable RSpec/MultipleExpectations end end describe 'error_class_for' do - it 'should return the correct class for code 40001' do - classy = Discordrb::Errors.error_class_for(40_001) + it 'returns the correct class for code 40001' do + classy = described_class.error_class_for(40_001) expect(classy).to be(Discordrb::Errors::Unauthorized) end end describe Discordrb::Errors::Unauthorized do - it 'should exist' do - expect(Discordrb::Errors::Unauthorized).to be_a(Class) + it 'exists' do + expect(described_class).to be_a(Class) end - it 'should have the correct code' do - instance = Discordrb::Errors::Unauthorized.new('some message') + it 'has the correct code' do + instance = described_class.new('some message') expect(instance.code).to eq(40_001) end end diff --git a/spec/event_spec.rb b/spec/event_spec.rb deleted file mode 100644 index bdfa3dc52..000000000 --- a/spec/event_spec.rb +++ /dev/null @@ -1,368 +0,0 @@ -# frozen_string_literal: true - -require 'discordrb' - -describe Discordrb::Events do - describe Discordrb::Events::Negated do - it 'should initialize without errors' do - Discordrb::Events::Negated.new(:test) - end - - it 'should contain the passed object' do - negated = Discordrb::Events::Negated.new(:test) - expect(negated.object).to eq :test - end - end - - describe 'not!' do - it 'should return a Negated object' do - expect(not!(:test)).to be_a(Discordrb::Events::Negated) - end - - it 'should contain the correct value' do - expect(not!(:test).object).to eq :test - end - end - - describe 'matches_all' do - it 'should return true for a nil attribute' do - expect(Discordrb::Events.matches_all(nil, nil)).to eq true - end - - it 'should be truthy if the block is truthy' do - expect(Discordrb::Events.matches_all(:a, :e) { true }).to be_truthy - expect(Discordrb::Events.matches_all(:a, :e) { 1 }).to be_truthy - expect(Discordrb::Events.matches_all(:a, :e) { 0 }).to be_truthy - expect(Discordrb::Events.matches_all(:a, :e) { 'string' }).to be_truthy - expect(Discordrb::Events.matches_all(:a, :e) { false }).to_not be_truthy - end - - it 'should be falsey if the block is falsey' do - expect(Discordrb::Events.matches_all(:a, :e) { nil }).to be_falsy - expect(Discordrb::Events.matches_all(:a, :e) { false }).to be_falsy - expect(Discordrb::Events.matches_all(:a, :e) { 0 }).to_not be_falsy - end - - it 'should correctly pass the arguments given' do - Discordrb::Events.matches_all(:one, :two) do |a, e| - expect(a).to eq(:one) - expect(e).to eq(:two) - end - end - - it 'should correctly compare arguments for comparison blocks' do - expect(Discordrb::Events.matches_all(1, 1) { |a, e| a == e }).to be_truthy - expect(Discordrb::Events.matches_all(1, 0) { |a, e| a == e }).to be_falsy - expect(Discordrb::Events.matches_all(0, 1) { |a, e| a == e }).to be_falsy - expect(Discordrb::Events.matches_all(0, 0) { |a, e| a == e }).to be_truthy - expect(Discordrb::Events.matches_all(1, 1) { |a, e| a != e }).to be_falsy - expect(Discordrb::Events.matches_all(1, 0) { |a, e| a != e }).to be_truthy - expect(Discordrb::Events.matches_all(0, 1) { |a, e| a != e }).to be_truthy - expect(Discordrb::Events.matches_all(0, 0) { |a, e| a != e }).to be_falsy - end - - it 'should return the opposite results for negated arguments' do - expect(Discordrb::Events.matches_all(not!(:a), :e) { true }).to be_falsy - expect(Discordrb::Events.matches_all(not!(:a), :e) { 1 }).to be_falsy - expect(Discordrb::Events.matches_all(not!(:a), :e) { 0 }).to be_falsy - expect(Discordrb::Events.matches_all(not!(:a), :e) { 'string' }).to be_falsy - expect(Discordrb::Events.matches_all(not!(:a), :e) { false }).to_not be_falsy - expect(Discordrb::Events.matches_all(not!(:a), :e) { nil }).to be_truthy - expect(Discordrb::Events.matches_all(not!(:a), :e) { false }).to be_truthy - expect(Discordrb::Events.matches_all(not!(:a), :e) { 0 }).to_not be_truthy - expect(Discordrb::Events.matches_all(not!(1), 1) { |a, e| a == e }).to be_falsy - expect(Discordrb::Events.matches_all(not!(1), 0) { |a, e| a == e }).to be_truthy - expect(Discordrb::Events.matches_all(not!(0), 1) { |a, e| a == e }).to be_truthy - expect(Discordrb::Events.matches_all(not!(0), 0) { |a, e| a == e }).to be_falsy - expect(Discordrb::Events.matches_all(not!(1), 1) { |a, e| a != e }).to be_truthy - expect(Discordrb::Events.matches_all(not!(1), 0) { |a, e| a != e }).to be_falsy - expect(Discordrb::Events.matches_all(not!(0), 1) { |a, e| a != e }).to be_falsy - expect(Discordrb::Events.matches_all(not!(0), 0) { |a, e| a != e }).to be_truthy - end - - it 'should find one correct element inside arrays' do - expect(Discordrb::Events.matches_all([1, 2, 3], 1) { |a, e| a == e }).to be_truthy - expect(Discordrb::Events.matches_all([1, 2, 3], 2) { |a, e| a == e }).to be_truthy - expect(Discordrb::Events.matches_all([1, 2, 3], 3) { |a, e| a == e }).to be_truthy - expect(Discordrb::Events.matches_all([1, 2, 3], 4) { |a, e| a != e }).to be_truthy - end - - it 'should return false when nothing matches inside arrays' do - expect(Discordrb::Events.matches_all([1, 2, 3], 4) { |a, e| a == e }).to be_falsy - end - - it 'should return the respective opposite results for negated arrays' do - expect(Discordrb::Events.matches_all(not!([1, 2, 3]), 1) { |a, e| a == e }).to be_falsy - expect(Discordrb::Events.matches_all(not!([1, 2, 3]), 2) { |a, e| a == e }).to be_falsy - expect(Discordrb::Events.matches_all(not!([1, 2, 3]), 3) { |a, e| a == e }).to be_falsy - expect(Discordrb::Events.matches_all(not!([1, 2, 3]), 4) { |a, e| a != e }).to be_falsy - expect(Discordrb::Events.matches_all(not!([1, 2, 3]), 4) { |a, e| a == e }).to be_truthy - end - end - - describe Discordrb::Events::MessageEvent do - let(:bot) { double } - let(:channel) { double } - let(:message) { double('message', channel: channel) } - - subject :event do - described_class.new(message, bot) - end - - describe 'after_call' do - subject :handler do - Discordrb::Events::MessageEventHandler.new(double, double('proc')) - end - - it 'calls send_file with attached file, filename, and spoiler' do - file = double(:file) - filename = double(:filename) - spoiler = double(:spoiler) - allow(file).to receive(:is_a?).with(File).and_return(true) - - expect(event).to receive(:send_file).with(file, caption: '', filename: filename, spoiler: spoiler) - event.attach_file(file, filename: filename, spoiler: spoiler) - handler.after_call(event) - end - end - end - - describe Discordrb::Events::EventHandler do - describe 'matches?' do - it 'should raise an error' do - expect { Discordrb::Events::EventHandler.new({}, nil).matches?(nil) }.to raise_error(RuntimeError) - end - end - end - - describe Discordrb::Events::TrueEventHandler do - describe 'matches?' do - it 'should return true' do - expect(Discordrb::Events::TrueEventHandler.new({}, nil).matches?(nil)).to eq true - end - - it 'should always call the block given' do - count = 0 - Discordrb::Events::TrueEventHandler.new({}, proc { count += 1 }).match(nil) - Discordrb::Events::TrueEventHandler.new({}, proc { count += 2 }).match(1) - Discordrb::Events::TrueEventHandler.new({}, proc do |e| - expect(e).to eq(1) - count += 4 - end).match(1) - Discordrb::Events::TrueEventHandler.new({ a: :b }, proc { count += 8 }).match(1) - Discordrb::Events::TrueEventHandler.new(nil, proc { count += 16 }).match(1) - end - end - end - - describe Discordrb::Events::MessageEventHandler do - describe 'matches?' do - it 'should call with empty attributes' do - t = track('empty attributes') - event = double('Discordrb::Events::MessageEvent') - Discordrb::Events::MessageEventHandler.new({}, proc { t.track(1) }).match(event) - # t.summary - end - end - - shared_examples 'end_with attributes' do |expr, matching, non_matching| - describe 'end_with attribute' do - it "matches #{matching}" do - handler = Discordrb::Events::MessageEventHandler.new({ end_with: expr }, double('proc')) - event = double('event', channel: double('channel', private?: false), author: double('author'), timestamp: double('timestamp'), content: matching) - allow(event).to receive(:is_a?).with(Discordrb::Events::MessageEvent).and_return(true) - expect(handler.matches?(event)).to be_truthy - end - - it "doesn't match #{non_matching}" do - handler = Discordrb::Events::MessageEventHandler.new({ end_with: expr }, double('proc')) - event = double('event', channel: double('channel', private?: false), author: double('author'), timestamp: double('timestamp'), content: non_matching) - allow(event).to receive(:is_a?).with(Discordrb::Events::MessageEvent).and_return(true) - expect(handler.matches?(event)).to be_falsy - end - end - end - - include_examples( - 'end_with attributes', /foo/, 'foo', 'f' - ) - - include_examples( - 'end_with attributes', /!$/, 'foo!', 'foo' - ) - - include_examples( - 'end_with attributes', /f(o)+/, 'foo', 'f' - ) - - include_examples( - 'end_with attributes', /e(fg)+(x(abba){1,2}x)*[stu]/i, 'abcdefgfgxabbaabbaxT', 'abcdefgfgxabbaabbaxT.' - ) - - include_examples( - 'end_with attributes', 'bar', 'foobar', 'foobarbaz' - ) - end - - # This data is shared across examples, so it needs to be defined here - # TODO: Refactor, potentially use `shared_context` - # rubocop:disable Lint/ConstantDefinitionInBlock - SERVER_ID = 1 - SERVER_NAME = 'server_name' - EMOJI1_ID = 10 - EMOJI1_NAME = 'emoji_name_1' - EMOJI2_ID = 11 - EMOJI2_NAME = 'emoji_name_2' - # rubocop:enable Lint/ConstantDefinitionInBlock - - shared_examples 'ServerEvent' do - describe '#initialize' do - it 'sets bot' do - expect(event.bot).to eq(bot) - end - it 'sets server' do - expect(event.server).to eq(server) - end - end - end - - shared_examples 'ServerEventHandler' do - describe '#matches?' do - it 'matches server names' do - handler = described_class.new({ server: SERVER_NAME }, nil) - expect(handler.matches?(event)).to be_truthy - end - - it 'matches server ids' do - handler = described_class.new({ server: SERVER_ID }, nil) - expect(handler.matches?(event)).to be_truthy - end - - it 'matches server object' do - handler = described_class.new({ server: server }, nil) - expect(handler.matches?(event)).to be_truthy - end - end - end - - shared_examples 'ServerEmojiEventHandler' do - describe '#matches?' do - it 'matches emoji id' do - handler = described_class.new({ id: EMOJI1_ID }, nil) - expect(handler.matches?(event)).to be_truthy - end - - it 'matches emoji name' do - handler = described_class.new({ name: EMOJI1_NAME }, nil) - expect(handler.matches?(event)).to be_truthy - end - end - end - - describe Discordrb::Events::ServerEvent do - let(:bot) { double('bot', server: server) } - let(:server) { double } - - subject(:event) do - described_class.new({ SERVER_ID => nil }, bot) - end - - it_behaves_like 'ServerEvent' - end - - describe Discordrb::Events::ServerEmojiCDEvent do - let(:bot) { double } - let(:server) { double } - let(:emoji) { double } - - subject(:event) do - described_class.new(server, emoji, bot) - end - - it_behaves_like 'ServerEvent' - - describe '#initialize' do - it 'sets emoji' do - expect(event.emoji).to eq(emoji) - end - end - end - - describe Discordrb::Events::ServerEmojiChangeEvent do - fixture :dispatch, %i[emoji dispatch] - - fixture_property :emoji_1_id, :dispatch, ['emojis', 0, 'id'], :to_i - fixture_property :emoji_2_id, :dispatch, ['emojis', 1, 'id'], :to_i - - let(:bot) { double } - let(:server) { double('server', emoji: { emoji_1_id => nil, emoji_2_id => nil }) } - - subject(:event) do - described_class.new(server, dispatch, bot) - end - - it_behaves_like 'ServerEvent' - - describe '#process_emoji' do - it 'sets an array of Emoji' do - expect(event.emoji).to eq([nil, nil]) - end - end - end - - describe Discordrb::Events::ServerEmojiUpdateEvent do - let(:bot) { double } - let(:server) { double } - let(:old_emoji) { double } - let(:emoji) { double } - - subject(:event) do - described_class.new(server, old_emoji, emoji, bot) - end - - it_behaves_like 'ServerEvent' - - describe '#initialize' do - it 'sets emoji' do - expect(event.emoji).to eq(emoji) - end - it 'sets old_emoji' do - expect(event.old_emoji).to eq(old_emoji) - end - end - end - - describe Discordrb::Events::ServerEventHandler do - let(:event) { double('event', is_a?: true, emoji: emoji, server: server) } - let(:server) { double('server', name: SERVER_NAME, id: SERVER_ID) } - let(:emoji) { double('emoji', id: EMOJI1_ID, name: EMOJI1_NAME) } - - it_behaves_like 'ServerEventHandler' - end - - describe Discordrb::Events::ServerEmojiCDEventHandler do - let(:event) { double('event', is_a?: true, emoji: emoji, server: server) } - let(:server) { double('server', name: SERVER_NAME, id: SERVER_ID) } - let(:emoji) { double('emoji', id: EMOJI1_ID, name: EMOJI1_NAME) } - - it_behaves_like 'ServerEventHandler' - it_behaves_like 'ServerEmojiEventHandler' - end - - describe Discordrb::Events::ServerEmojiUpdateEventHandler do - let(:event) { double('event', is_a?: true, emoji: emoji_new, old_emoji: emoji_old, server: server) } - let(:server) { double('server', name: SERVER_NAME, id: SERVER_ID) } - let(:emoji_old) { double('emoji_old', id: EMOJI1_ID, name: EMOJI2_NAME) } - let(:emoji_new) { double('emoji_new', name: EMOJI1_NAME) } - - it_behaves_like 'ServerEventHandler' - it_behaves_like 'ServerEmojiEventHandler' - - describe '#matches?' do - it 'matches old emoji name' do - handler = described_class.new({ old_name: EMOJI2_NAME }, nil) - expect(handler.matches?(event)).to be_truthy - end - end - end -end diff --git a/spec/events_spec.rb b/spec/events_spec.rb new file mode 100644 index 000000000..d12e2fd27 --- /dev/null +++ b/spec/events_spec.rb @@ -0,0 +1,375 @@ +# frozen_string_literal: true + +require 'discordrb' + +describe Discordrb::Events do + describe Discordrb::Events::Negated do + it 'initializes without errors' do + expect { described_class.new(:test) }.not_to raise_error + end + + it 'contains the passed object' do + negated = described_class.new(:test) + expect(negated.object).to eq :test + end + end + + describe 'not!' do + it 'returns a Negated object' do + expect(not!(:test)).to be_a(described_class::Negated) + end + + it 'contains the correct value' do + expect(not!(:test).object).to be :test + end + end + + describe '.matches_all' do + it 'returns true for a nil attribute' do + expect(described_class.matches_all(nil, nil)).to be true + end + + # TODO: Rework tests to not rely on multiple expectations + # rubocop:disable RSpec/MultipleExpectations + it 'is truthy if the block is truthy' do + expect(described_class.matches_all(:a, :e) { true }).to be_truthy + expect(described_class.matches_all(:a, :e) { 1 }).to be_truthy + expect(described_class.matches_all(:a, :e) { 0 }).to be_truthy + expect(described_class.matches_all(:a, :e) { 'string' }).to be_truthy + expect(described_class.matches_all(:a, :e) { false }).not_to be_truthy + end + + it 'is falsey if the block is falsey' do + expect(described_class.matches_all(:a, :e) { nil }).to be_falsy + expect(described_class.matches_all(:a, :e) { false }).to be_falsy + expect(described_class.matches_all(:a, :e) { 0 }).not_to be_falsy + end + + it 'correctly passes the arguments given' do + described_class.matches_all(:one, :two) do |a, e| + expect(a).to be :one + expect(e).to be :two + end + end + + it 'correctly compares arguments for comparison blocks' do + expect(described_class.matches_all(1, 1) { |a, e| a == e }).to be_truthy + expect(described_class.matches_all(1, 0) { |a, e| a == e }).to be_falsy + expect(described_class.matches_all(0, 1) { |a, e| a == e }).to be_falsy + expect(described_class.matches_all(0, 0) { |a, e| a == e }).to be_truthy + expect(described_class.matches_all(1, 1) { |a, e| a != e }).to be_falsy + expect(described_class.matches_all(1, 0) { |a, e| a != e }).to be_truthy + expect(described_class.matches_all(0, 1) { |a, e| a != e }).to be_truthy + expect(described_class.matches_all(0, 0) { |a, e| a != e }).to be_falsy + end + + it 'returns the opposite results for negated arguments' do + expect(described_class.matches_all(not!(:a), :e) { true }).to be_falsy + expect(described_class.matches_all(not!(:a), :e) { 1 }).to be_falsy + expect(described_class.matches_all(not!(:a), :e) { 0 }).to be_falsy + expect(described_class.matches_all(not!(:a), :e) { 'string' }).to be_falsy + expect(described_class.matches_all(not!(:a), :e) { false }).not_to be_falsy + expect(described_class.matches_all(not!(:a), :e) { nil }).to be_truthy + expect(described_class.matches_all(not!(:a), :e) { false }).to be_truthy + expect(described_class.matches_all(not!(:a), :e) { 0 }).not_to be_truthy + expect(described_class.matches_all(not!(1), 1) { |a, e| a == e }).to be_falsy + expect(described_class.matches_all(not!(1), 0) { |a, e| a == e }).to be_truthy + expect(described_class.matches_all(not!(0), 1) { |a, e| a == e }).to be_truthy + expect(described_class.matches_all(not!(0), 0) { |a, e| a == e }).to be_falsy + expect(described_class.matches_all(not!(1), 1) { |a, e| a != e }).to be_truthy + expect(described_class.matches_all(not!(1), 0) { |a, e| a != e }).to be_falsy + expect(described_class.matches_all(not!(0), 1) { |a, e| a != e }).to be_falsy + expect(described_class.matches_all(not!(0), 0) { |a, e| a != e }).to be_truthy + end + + it 'finds one correct element inside arrays' do + expect(described_class.matches_all([1, 2, 3], 1) { |a, e| a == e }).to be_truthy + expect(described_class.matches_all([1, 2, 3], 2) { |a, e| a == e }).to be_truthy + expect(described_class.matches_all([1, 2, 3], 3) { |a, e| a == e }).to be_truthy + expect(described_class.matches_all([1, 2, 3], 4) { |a, e| a != e }).to be_truthy + end + + it 'returns false when nothing matches inside arrays' do + expect(described_class.matches_all([1, 2, 3], 4) { |a, e| a == e }).to be_falsy + end + + it 'returns the respective opposite results for negated arrays' do + expect(described_class.matches_all(not!([1, 2, 3]), 1) { |a, e| a == e }).to be_falsy + expect(described_class.matches_all(not!([1, 2, 3]), 2) { |a, e| a == e }).to be_falsy + expect(described_class.matches_all(not!([1, 2, 3]), 3) { |a, e| a == e }).to be_falsy + expect(described_class.matches_all(not!([1, 2, 3]), 4) { |a, e| a != e }).to be_falsy + expect(described_class.matches_all(not!([1, 2, 3]), 4) { |a, e| a == e }).to be_truthy + end + # rubocop:enable RSpec/MultipleExpectations + end + + describe Discordrb::Events::MessageEvent do + subject :event do + described_class.new(message, bot) + end + + let(:bot) { double } + let(:channel) { double } + let(:message) { instance_double(Discordrb::Message, 'message', channel: channel) } + + describe 'after_call' do + subject :handler do + Discordrb::Events::MessageEventHandler.new(double, double) + end + + it 'calls send_file with attached file, filename, and spoiler' do + file = double + filename = double + spoiler = double + allow(file).to receive(:is_a?).with(File).and_return(true) + allow(event).to receive(:send_file) + + event.attach_file(file, filename: filename, spoiler: spoiler) + handler.after_call(event) + expect(event).to have_received(:send_file).with(file, caption: '', filename: filename, spoiler: spoiler) + end + end + end + + describe Discordrb::Events::EventHandler do + describe 'matches?' do + it 'raises an error' do + expect { described_class.new({}, nil).matches?(nil) }.to raise_error(RuntimeError) + end + end + end + + describe Discordrb::Events::TrueEventHandler do + describe 'matches?' do + it 'returns true' do + expect(described_class.new({}, nil).matches?(nil)).to be true + end + + it 'always calls the block given' do + count = 0 + described_class.new({}, proc { count += 1 }).match(nil) + described_class.new({}, proc { count += 2 }).match(1) + described_class.new({}, proc do |e| + expect(e).to eq(1) + count += 4 + end).match(1) + described_class.new({ a: :b }, proc { count += 8 }).match(1) + described_class.new(nil, proc { count += 16 }).match(1) + end + end + end + + describe Discordrb::Events::MessageEventHandler do + describe 'matches?' do + it 'calls with empty attributes', skip: 'meew0 disabled in 2cf0a42' do + t = track('empty attributes') + event = double + described_class.new({}, proc { t.track(1) }).match(event) + t.summary + end + end + + shared_examples 'end_with attributes' do |expr, matching, non_matching| + describe 'end_with attribute' do + it "matches #{matching}" do + handler = described_class.new({ end_with: expr }, double) + event = instance_double(Discordrb::Events::MessageEvent, 'event', channel: instance_double(Discordrb::Channel, 'channel', private?: false), author: double, timestamp: double, content: matching) + allow(event).to receive(:is_a?).with(Discordrb::Events::MessageEvent).and_return(true) + expect(handler).to be_matching(event) + end + + it "doesn't match #{non_matching}" do + handler = described_class.new({ end_with: expr }, double) + event = instance_double(Discordrb::Events::MessageEvent, 'event', channel: instance_double(Discordrb::Channel, 'channel', private?: false), author: double, timestamp: double, content: non_matching) + allow(event).to receive(:is_a?).with(Discordrb::Events::MessageEvent).and_return(true) + expect(handler).not_to be_matching(event) + end + end + end + + include_examples( + 'end_with attributes', /foo/, 'foo', 'f' + ) + + include_examples( + 'end_with attributes', /!$/, 'foo!', 'foo' + ) + + include_examples( + 'end_with attributes', /f(o)+/, 'foo', 'f' + ) + + include_examples( + 'end_with attributes', /e(fg)+(x(abba){1,2}x)*[stu]/i, 'abcdefgfgxabbaabbaxT', 'abcdefgfgxabbaabbaxT.' + ) + + include_examples( + 'end_with attributes', 'bar', 'foobar', 'foobarbaz' + ) + end + + shared_context 'with bot and server' do + let(:server_name) { 'server_name' } + let(:server_id) { 1 } + let(:server) { instance_double(Discordrb::Server, 'server', name: server_name, id: server_id) } + let(:bot) { instance_double(Discordrb::Bot, 'bot', server: server) } + end + + shared_context 'with emoji' do + let(:emoji_id) { 10 } + let(:emoji_name) { 'emoji_name_1' } + let(:emoji) { instance_double(Discordrb::Emoji, 'emoji', id: emoji_id, name: emoji_name) } + end + + shared_examples 'ServerEvent' do + describe '#initialize' do + it 'sets bot' do + expect(event.bot).to eq(bot) + end + + it 'sets server' do + expect(event.server).to eq(server) + end + end + end + + shared_examples 'ServerEventHandler' do + describe '#matches?' do + it 'matches server names' do + handler = described_class.new({ server: server_name }, nil) + expect(handler).to be_matching(event) + end + + it 'matches server ids' do + handler = described_class.new({ server: server_id }, nil) + expect(handler).to be_matching(event) + end + + it 'matches server object' do + handler = described_class.new({ server: server }, nil) + expect(handler).to be_matching(event) + end + end + end + + shared_examples 'ServerEmojiEventHandler' do + describe '#matches?' do + it 'matches emoji id' do + handler = described_class.new({ id: emoji_id }, nil) + expect(handler).to be_matching(event) + end + + it 'matches emoji name' do + handler = described_class.new({ name: emoji_name }, nil) + expect(handler).to be_matching(event) + end + end + end + + describe Discordrb::Events::ServerEvent do + subject(:event) do + described_class.new({ server_id => nil }, bot) + end + + include_context 'with bot and server' + + it_behaves_like 'ServerEvent' + end + + describe Discordrb::Events::ServerEmojiCDEvent do + subject(:event) do + described_class.new(server, emoji, bot) + end + + include_context 'with bot and server' + let(:emoji) { double } + + it_behaves_like 'ServerEvent' + + describe '#initialize' do + it 'sets emoji' do + expect(event.emoji).to eq(emoji) + end + end + end + + describe Discordrb::Events::ServerEmojiChangeEvent do + subject(:event) do + described_class.new(server, dispatch, bot) + end + + include_context 'with bot and server' + before do + allow(server).to receive(:emoji).and_return({ emoji_1_id => nil, emoji_2_id => nil }) + end + + fixture :dispatch, %i[emoji dispatch] + + fixture_property :emoji_1_id, :dispatch, ['emojis', 0, 'id'], :to_i + fixture_property :emoji_2_id, :dispatch, ['emojis', 1, 'id'], :to_i + + it_behaves_like 'ServerEvent' + + describe '#process_emoji' do + it 'sets an array of Emoji' do + expect(event.emoji).to eq([nil, nil]) + end + end + end + + describe Discordrb::Events::ServerEmojiUpdateEvent do + subject(:event) do + described_class.new(server, old_emoji, emoji, bot) + end + + include_context 'with bot and server' + let(:old_emoji) { double } + let(:emoji) { double } + + it_behaves_like 'ServerEvent' + + describe '#initialize' do + it 'sets emoji' do + expect(event.emoji).to eq(emoji) + end + + it 'sets old_emoji' do + expect(event.old_emoji).to eq(old_emoji) + end + end + end + + describe Discordrb::Events::ServerEventHandler do + include_context 'with bot and server' + let(:event) { instance_double(Discordrb::Events::ServerEvent, 'event', is_a?: true, server: server) } + + include_examples 'ServerEventHandler' + end + + describe Discordrb::Events::ServerEmojiCDEventHandler do + include_context 'with bot and server' + include_context 'with emoji' + let(:event) { instance_double(Discordrb::Events::ServerEmojiCDEvent, 'event', is_a?: true, emoji: emoji, server: server) } + + it_behaves_like 'ServerEventHandler' + it_behaves_like 'ServerEmojiEventHandler' + end + + describe Discordrb::Events::ServerEmojiUpdateEventHandler do + include_context 'with bot and server' + include_context 'with emoji' + let(:emoji_name_two) { 'emoji_name_2' } + let(:event) { instance_double(Discordrb::Events::ServerEmojiUpdateEvent, 'event', is_a?: true, emoji: emoji, old_emoji: old_emoji, server: server) } + let(:old_emoji) { instance_double(Discordrb::Emoji, 'old emoji', id: emoji_id, name: emoji_name_two) } + + it_behaves_like 'ServerEventHandler' + it_behaves_like 'ServerEmojiEventHandler' + + describe '#matches?' do + it 'matches old emoji name' do + handler = described_class.new({ old_name: emoji_name_two }, nil) + expect(handler).to be_matching(event) + end + end + end +end diff --git a/spec/helpers_spec.rb b/spec/helpers_spec.rb deleted file mode 100644 index b243aea01..000000000 --- a/spec/helpers_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -describe '#load_data_file' do - it 'should load the test data correctly' do - data = load_data_file(:test) - expect(data['success']).to eq(true) - end -end - -describe '#fixture' do - fixture :data, [:test] - - it 'should load the test data correctly' do - expect(data['success']).to eq(true) - end -end - -describe '#fixture_property' do - fixture :data, [:test] - fixture_property :data_success, :data, ['success'] - fixture_property :data_success_str, :data, ['success'], :to_s - - it 'should define the test property correctly' do - expect(data_success).to eq(true) - end - - it 'should filter data correctly' do - expect(data_success_str).to eq('true') - end -end diff --git a/spec/logger_spec.rb b/spec/logger_spec.rb index 7dd8b465c..2a1a35900 100644 --- a/spec/logger_spec.rb +++ b/spec/logger_spec.rb @@ -3,29 +3,29 @@ require 'discordrb' describe Discordrb::Logger do - it 'should log messages' do + it 'logs messages' do stream = spy - logger = Discordrb::Logger.new(false, [stream]) + logger = described_class.new(false, [stream]) logger.error('Testing') expect(stream).to have_received(:puts).with(something_including('Testing')) end - it 'should respect the log mode' do + it 'respects the log mode' do stream = spy - logger = Discordrb::Logger.new(false, [stream]) + logger = described_class.new(false, [stream]) logger.mode = :silent logger.error('Testing') - expect(stream).to_not have_received(:puts) + expect(stream).not_to have_received(:puts) end - context 'fancy mode' do - it 'should log messages' do + context 'with fancy mode' do + it 'logs messages' do stream = spy - logger = Discordrb::Logger.new(true, [stream]) + logger = described_class.new(true, [stream]) logger.error('Testing') @@ -33,10 +33,10 @@ end end - context 'redacted token' do - it 'should redact the token from messages' do + context 'with a redacted token' do + it 'redacts the token from messages' do stream = spy - logger = Discordrb::Logger.new(true, [stream]) + logger = described_class.new(true, [stream]) logger.token = 'asdfg' logger.error('this message contains a token that should be redacted: asdfg') diff --git a/spec/main_spec.rb b/spec/main_spec.rb index 8b10d4d70..e8ccede60 100644 --- a/spec/main_spec.rb +++ b/spec/main_spec.rb @@ -11,17 +11,19 @@ def initialize(id) end describe Discordrb do - it 'should split messages correctly' do - split = Discordrb.split_message('a' * 5234) + # TODO: Rework tests to not rely on multiple expectations + # rubocop:disable RSpec/MultipleExpectations + it 'splits messages correctly' do + split = described_class.split_message('a' * 5234) expect(split).to eq(['a' * 2000, 'a' * 2000, 'a' * 1234]) - split_on_space = Discordrb.split_message("#{'a' * 1990} #{'b' * 2000}") + split_on_space = described_class.split_message("#{'a' * 1990} #{'b' * 2000}") expect(split_on_space).to eq(["#{'a' * 1990} ", 'b' * 2000]) # regression test # there had been an issue where this would have raised an error, # and (if it hadn't raised) produced incorrect results - split = Discordrb.split_message("#{'a' * 800}\n" * 6) + split = described_class.split_message("#{'a' * 800}\n" * 6) expect(split).to eq([ "#{'a' * 800}\n#{'a' * 800}\n", "#{'a' * 800}\n#{'a' * 800}\n", @@ -31,23 +33,24 @@ def initialize(id) describe Discordrb::IDObject do describe '#==' do - it 'should match identical values' do + it 'matches identical values' do ido = SimpleIDObject.new(123) - expect(ido == SimpleIDObject.new(123)).to eq(true) - expect(ido == 123).to eq(true) - expect(ido == '123').to eq(true) + expect(ido == SimpleIDObject.new(123)).to be true + expect(ido == 123).to be true + expect(ido == '123').to be true end - it 'should not match different values' do + it 'doesn\'t match different values' do ido = SimpleIDObject.new(123) - expect(ido == SimpleIDObject.new(124)).to eq(false) - expect(ido == 124).to eq(false) - expect(ido == '124').to eq(false) + expect(ido == SimpleIDObject.new(124)).to be false + expect(ido == 124).to be false + expect(ido == '124').to be false end + # rubocop:enable RSpec/MultipleExpectations end describe '#creation_time' do - it 'should return the correct time' do + it 'returns the correct time' do ido = SimpleIDObject.new(175_928_847_299_117_063) time = Time.new(2016, 4, 30, 11, 18, 25.796, 0) expect(ido.creation_time.utc).to be_within(0.0001).of(time) @@ -55,15 +58,15 @@ def initialize(id) end describe '.synthesise' do - it 'should match a precalculated time' do + it 'matches a precalculated time' do snowflake = 175_928_847_298_985_984 time = Time.new(2016, 4, 30, 11, 18, 25.796, 0) - expect(Discordrb::IDObject.synthesise(time)).to eq(snowflake) + expect(described_class.synthesise(time)).to eq(snowflake) end - it 'should match #creation_time' do + it 'matches #creation_time' do time = Time.new(2016, 4, 30, 11, 18, 25.796, 0) - ido = SimpleIDObject.new(Discordrb::IDObject.synthesise(time)) + ido = SimpleIDObject.new(described_class.synthesise(time)) expect(ido.creation_time).to be_within(0.0001).of(time) end end diff --git a/spec/overwrite_spec.rb b/spec/overwrite_spec.rb index 0ced52e45..70339f9e1 100644 --- a/spec/overwrite_spec.rb +++ b/spec/overwrite_spec.rb @@ -107,11 +107,14 @@ expect(copy).to eq original end + # TODO: Rework test to not rely on multiple expectations + # rubocop:disable RSpec/MultipleExpectations it 'creates new permission objects' do copy = described_class.from_other(original) expect(copy.allow).not_to be original.allow expect(copy.deny).not_to be original.deny end + # rubocop:enable RSpec/MultipleExpectations end end diff --git a/spec/paginator_spec.rb b/spec/paginator_spec.rb index 68a0cc46a..7d321a5f6 100644 --- a/spec/paginator_spec.rb +++ b/spec/paginator_spec.rb @@ -3,7 +3,9 @@ require 'discordrb' describe Discordrb::Paginator do - context 'direction down' do + # TODO: Rework tests to not rely on multiple expectations + # rubocop:disable RSpec/MultipleExpectations + context 'when direction is down' do it 'requests all pages until empty' do data = [ [1, 2, 3], @@ -13,7 +15,7 @@ ] index = 0 - paginator = Discordrb::Paginator.new(nil, :down) do |last_page| + paginator = described_class.new(nil, :down) do |last_page| expect(last_page).to eq data[index - 1] if last_page next_page = data[index] index += 1 @@ -24,7 +26,7 @@ end end - context 'direction up' do + context 'when direction is up' do it 'requests all pages until empty' do data = [ [6, 7], @@ -34,7 +36,7 @@ ] index = 0 - paginator = Discordrb::Paginator.new(nil, :up) do |last_page| + paginator = described_class.new(nil, :up) do |last_page| expect(last_page).to eq data[index - 1] if last_page next_page = data[index] index += 1 @@ -44,6 +46,7 @@ expect(paginator.to_a).to eq [7, 6, 5, 4] end end + # rubocop:enable RSpec/MultipleExpectations it 'only returns up to limit items' do data = [ @@ -53,7 +56,7 @@ ] index = 0 - paginator = Discordrb::Paginator.new(2, :down) do |_last_page| + paginator = described_class.new(2, :down) do |_last_page| next_page = data[index] index += 1 next_page diff --git a/spec/permission_calculator_spec.rb b/spec/permission_calculator_spec.rb new file mode 100644 index 000000000..0c29cb04a --- /dev/null +++ b/spec/permission_calculator_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'discordrb' + +class ExampleCalculator + include Discordrb::PermissionCalculator + attr_accessor :server, :roles +end + +describe Discordrb::PermissionCalculator do + subject(:calculator) { ExampleCalculator.new } + + # TODO: Rework tests to not rely on multiple expectations + # rubocop:disable RSpec/MultipleExpectations + describe '#defined_role_permission?' do + it 'solves permissions (issue #607)' do + everyone_role = instance_double(Discordrb::Role, 'everyone role', id: 0, position: 0, permissions: Discordrb::Permissions.new) + role_a = instance_double(Discordrb::Role, 'role a', id: 1, position: 1, permissions: Discordrb::Permissions.new) + role_b = instance_double(Discordrb::Role, 'role b', id: 2, position: 2, permissions: Discordrb::Permissions.new([:manage_messages])) + + channel = double + allow(calculator).to receive(:permission_overwrite) + .with(:manage_messages, channel, everyone_role.id) + .and_return(false) + + allow(calculator).to receive(:permission_overwrite) + .with(:manage_messages, channel, role_a.id) + .and_return(true) + + allow(calculator).to receive(:permission_overwrite) + .with(:manage_messages, channel, role_b.id) + .and_return(false) + + calculator.server = instance_double(Discordrb::Server, 'server', everyone_role: everyone_role) + calculator.roles = [role_a, role_b] + permission = calculator.__send__(:defined_role_permission?, :manage_messages, channel) + expect(permission).to be true + + calculator.roles = [role_b, role_a] + permission = calculator.__send__(:defined_role_permission?, :manage_messages, channel) + expect(permission).to be true + end + + it 'takes overwrites into account' do + everyone_role = instance_double(Discordrb::Role, 'everyone role', id: 0, position: 0, permissions: Discordrb::Permissions.new) + role_a = instance_double(Discordrb::Role, 'role a', id: 1, position: 1, permissions: Discordrb::Permissions.new([:manage_messages])) + role_b = instance_double(Discordrb::Role, 'role b', id: 2, position: 2, permissions: Discordrb::Permissions.new) + channel = double + + calculator.server = instance_double(Discordrb::Server, 'server', everyone_role: everyone_role) + calculator.roles = [role_a, role_b] + + allow(calculator).to receive(:permission_overwrite).and_return(nil) + + allow(calculator).to receive(:permission_overwrite) + .with(:manage_messages, channel, role_a.id) + .and_return(:deny) + + allow(calculator).to receive(:permission_overwrite) + .with(:manage_messages, channel, role_b.id) + .and_return(:allow) + + calculator.roles = [role_a] + expect(calculator.__send__(:defined_role_permission?, :manage_messages, channel)).to be false + + calculator.roles = [role_a, role_b] + expect(calculator.__send__(:defined_role_permission?, :manage_messages, channel)).to be true + end + end + # rubocop:enable RSpec/MultipleExpectations +end diff --git a/spec/permissions_spec.rb b/spec/permissions_spec.rb index ac0767d7f..a2c74aafa 100644 --- a/spec/permissions_spec.rb +++ b/spec/permissions_spec.rb @@ -3,22 +3,22 @@ require 'discordrb' describe Discordrb::Permissions do - subject { Discordrb::Permissions.new } + subject(:permissions) { described_class.new } - describe Discordrb::Permissions::FLAGS do + describe '::FLAGS' do it 'creates a setter for each flag' do - responds_to_methods = Discordrb::Permissions::FLAGS.map do |_, flag| - subject.respond_to?(:"can_#{flag}=") + responds_to_methods = described_class::FLAGS.map do |_, flag| + permissions.respond_to?(:"can_#{flag}=") end - expect(responds_to_methods.all?).to eq true + expect(responds_to_methods.all?).to be true end it 'calls #write on its writer' do - writer = double - expect(writer).to receive(:write) + writer = instance_double(Discordrb::Role::RoleWriter, 'writer', write: nil) - Discordrb::Permissions.new(0, writer).can_read_messages = true + described_class.new(0, writer).can_read_messages = true + expect(writer).to have_received(:write) end end @@ -31,8 +31,8 @@ it 'sets an attribute for each flag' do expect( [ - subject.instance_variable_get(:@foo), - subject.instance_variable_get(:@bar) + permissions.instance_variable_get(:@foo), + permissions.instance_variable_get(:@bar) ] ).to eq [false, false] end @@ -40,109 +40,49 @@ describe '.bits' do it 'returns the correct packed bits from an array of symbols' do - expect(Discordrb::Permissions.bits(%i[foo bar])).to eq 3 + expect(described_class.bits(%i[foo bar])).to eq 3 end end describe '#bits=' do + before do + allow(permissions).to receive(:init_vars) + end + it 'updates the cached value' do - allow(subject).to receive(:init_vars) - subject.bits = 1 - expect(subject.bits).to eq(1) + permissions.bits = 1 + expect(permissions.bits).to eq(1) end it 'calls #init_vars' do - expect(subject).to receive(:init_vars) - subject.bits = 0 + permissions.bits = 0 + expect(permissions).to have_received(:init_vars) end end describe '#initialize' do it 'initializes with 0 bits' do - expect(subject.bits).to eq 0 + expect(permissions.bits).to eq 0 end it 'can initialize with an array of symbols' do - instance = Discordrb::Permissions.new %i[foo bar] + instance = described_class.new %i[foo bar] expect(instance.bits).to eq 3 end it 'calls #init_vars' do - expect_any_instance_of(Discordrb::Permissions).to receive(:init_vars) - subject + permissions = described_class.allocate + allow(permissions).to receive(:init_vars) + permissions.send :initialize + expect(permissions).to have_received(:init_vars) end end describe '#defined_permissions' do it 'returns the defined permissions' do - instance = Discordrb::Permissions.new 3 + instance = described_class.new 3 expect(instance.defined_permissions).to eq %i[foo bar] end end end end - -class ExampleCalculator - include Discordrb::PermissionCalculator - attr_accessor :server, :roles -end - -describe Discordrb::PermissionCalculator do - subject { ExampleCalculator.new } - - describe '#defined_role_permission?' do - it 'solves permissions (issue #607)' do - everyone_role = double('everyone role', id: 0, position: 0, permissions: Discordrb::Permissions.new) - role_a = double('role a', id: 1, position: 1, permissions: Discordrb::Permissions.new) - role_b = double('role b', id: 2, position: 2, permissions: Discordrb::Permissions.new([:manage_messages])) - - channel = double('channel') - allow(subject).to receive(:permission_overwrite) - .with(:manage_messages, channel, everyone_role.id) - .and_return(false) - - allow(subject).to receive(:permission_overwrite) - .with(:manage_messages, channel, role_a.id) - .and_return(true) - - allow(subject).to receive(:permission_overwrite) - .with(:manage_messages, channel, role_b.id) - .and_return(false) - - subject.server = double('server', everyone_role: everyone_role) - subject.roles = [role_a, role_b] - permission = subject.__send__(:defined_role_permission?, :manage_messages, channel) - expect(permission).to eq true - - subject.roles = [role_b, role_a] - permission = subject.__send__(:defined_role_permission?, :manage_messages, channel) - expect(permission).to eq true - end - - it 'takes overwrites into account' do - everyone_role = double('everyone role', id: 0, position: 0, permissions: Discordrb::Permissions.new) - role_a = double('role a', id: 1, position: 1, permissions: Discordrb::Permissions.new([:manage_messages])) - role_b = double('role b', id: 2, position: 2, permissions: Discordrb::Permissions.new) - channel = double('channel') - - subject.server = double('server', everyone_role: everyone_role) - subject.roles = [role_a, role_b] - - allow(subject).to receive(:permission_overwrite).and_return(nil) - - allow(subject).to receive(:permission_overwrite) - .with(:manage_messages, channel, role_a.id) - .and_return(:deny) - - allow(subject).to receive(:permission_overwrite) - .with(:manage_messages, channel, role_b.id) - .and_return(:allow) - - subject.roles = [role_a] - expect(subject.__send__(:defined_role_permission?, :manage_messages, channel)).to be false - - subject.roles = [role_a, role_b] - expect(subject.__send__(:defined_role_permission?, :manage_messages, channel)).to be true - end - end -end diff --git a/spec/rate_limiter_spec.rb b/spec/rate_limiter_spec.rb deleted file mode 100644 index 2c4cf6c5e..000000000 --- a/spec/rate_limiter_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -# frozen_string_literal: true - -require 'discordrb' - -# alias so I don't have to type it out every time... -BUCKET = Discordrb::Commands::Bucket -RATELIMITER = Discordrb::Commands::RateLimiter - -describe Discordrb::Commands::Bucket do - describe 'rate_limited?' do - it 'should not rate limit one request' do - expect(BUCKET.new(1, 5, 2).rate_limited?(:a)).to be_falsy - expect(BUCKET.new(nil, nil, 2).rate_limited?(:a)).to be_falsy - expect(BUCKET.new(1, 5, nil).rate_limited?(:a)).to be_falsy - expect(BUCKET.new(0, 1, nil).rate_limited?(:a)).to be_falsy - expect(BUCKET.new(0, 1_000_000_000, 500_000_000).rate_limited?(:a)).to be_falsy - end - - it 'should fail to initialize with invalid arguments' do - expect { BUCKET.new(0, nil, 0) }.to raise_error(ArgumentError) - end - - it 'should fail to rate limit something invalid' do - expect { BUCKET.new(1, 5, 2).rate_limited?("can't RL a string!") }.to raise_error(ArgumentError) - end - - it 'should rate limit one request over the limit' do - b = BUCKET.new(1, 5, nil) - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_truthy - end - - it 'should rate limit multiple requests that are over the limit' do - b = BUCKET.new(3, 5, nil) - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_truthy - end - - it 'should allow to be passed a custom increment' do - b = BUCKET.new(5, 5, nil) - expect(b.rate_limited?(:a, increment: 2)).to be_falsy - expect(b.rate_limited?(:a, increment: 2)).to be_falsy - expect(b.rate_limited?(:a, increment: 2)).to be_truthy - end - - it 'should not rate limit after the limit ran out' do - b = BUCKET.new(2, 5, nil) - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 4)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 5)).to be_falsy - end - - it 'should reset the limit after it ran out' do - b = BUCKET.new(2, 5, nil) - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 5)).to be_falsy - expect(b.rate_limited?(:a, Time.now + 5.01)).to be_falsy - expect(b.rate_limited?(:a, Time.now + 5.02)).to be_truthy - end - - it 'should rate limit based on delay' do - b = BUCKET.new(nil, nil, 2) - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_truthy - end - - it 'should not rate limit after the delay ran out' do - b = BUCKET.new(nil, nil, 2) - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 2)).to be_falsy - expect(b.rate_limited?(:a, Time.now + 2)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 4)).to be_falsy - expect(b.rate_limited?(:a, Time.now + 4)).to be_truthy - end - - it 'should rate limit based on both limit and delay' do - b = BUCKET.new(2, 5, 2) - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 2)).to be_falsy - expect(b.rate_limited?(:a, Time.now + 2)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 4)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 5)).to be_falsy - expect(b.rate_limited?(:a, Time.now + 6)).to be_truthy - - b = BUCKET.new(2, 5, 2) - expect(b.rate_limited?(:a)).to be_falsy - expect(b.rate_limited?(:a)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 4)).to be_falsy - expect(b.rate_limited?(:a, Time.now + 4)).to be_truthy - expect(b.rate_limited?(:a, Time.now + 5)).to be_truthy - end - - it 'should return correct times' do - start_time = Time.now - b = BUCKET.new(2, 5, 2) - expect(b.rate_limited?(:a, start_time)).to be_falsy - expect(b.rate_limited?(:a, start_time).round(2)).to eq(2) - expect(b.rate_limited?(:a, start_time + 1).round(2)).to eq(1) - expect(b.rate_limited?(:a, start_time + 2.01)).to be_falsy - expect(b.rate_limited?(:a, start_time + 2).round(2)).to eq(3) - end - end -end diff --git a/spec/rspec_helpers_spec.rb b/spec/rspec_helpers_spec.rb new file mode 100644 index 000000000..3b2703a74 --- /dev/null +++ b/spec/rspec_helpers_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +describe Rspec do + describe '#load_data_file' do + it 'loads the test data correctly' do + data = load_data_file(:test) + expect(data['success']).to be true + end + end + + describe '#fixture' do + fixture :data, [:test] + + it 'loads the test data correctly' do + expect(data['success']).to be true + end + end + + describe '#fixture_property' do + fixture :data, [:test] + fixture_property :data_success, :data, ['success'] + fixture_property :data_success_str, :data, ['success'], :to_s + + it 'defines the test property correctly' do + expect(data_success).to be true + end + + it 'filters data correctly' do + expect(data_success_str).to be 'true' + end + end +end diff --git a/spec/sodium_spec.rb b/spec/sodium_spec.rb deleted file mode 100644 index d779956d4..000000000 --- a/spec/sodium_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -require 'discordrb/voice/sodium' - -describe Discordrb::Voice::SecretBox do - def rand_bytes(size) - bytes = Array.new(size) { rand(256) } - bytes.pack('C*') - end - - it 'encrypts round trip' do - key = rand_bytes(Discordrb::Voice::SecretBox::KEY_LENGTH) - nonce = rand_bytes(Discordrb::Voice::SecretBox::NONCE_BYTES) - message = rand_bytes(20) - - secret_box = Discordrb::Voice::SecretBox.new(key) - ct = secret_box.box(nonce, message) - pt = secret_box.open(nonce, ct) - expect(pt).to eq message - end - - it 'raises on invalid key length' do - key = rand_bytes(Discordrb::Voice::SecretBox::KEY_LENGTH - 1) - expect { Discordrb::Voice::SecretBox.new(key) }.to raise_error(Discordrb::Voice::SecretBox::LengthError) - end - - describe '#box' do - it 'raises on invalid nonce length' do - key = rand_bytes(Discordrb::Voice::SecretBox::KEY_LENGTH) - nonce = rand_bytes(Discordrb::Voice::SecretBox::NONCE_BYTES - 1) - expect { Discordrb::Voice::SecretBox.new(key).box(nonce, '') }.to raise_error(Discordrb::Voice::SecretBox::LengthError) - end - end - - describe '#open' do - it 'raises on invalid nonce length' do - key = rand_bytes(Discordrb::Voice::SecretBox::KEY_LENGTH) - nonce = rand_bytes(Discordrb::Voice::SecretBox::NONCE_BYTES - 1) - expect { Discordrb::Voice::SecretBox.new(key).open(nonce, '') }.to raise_error(Discordrb::Voice::SecretBox::LengthError) - end - end -end diff --git a/spec/voice/secret_box_spec.rb b/spec/voice/secret_box_spec.rb new file mode 100644 index 000000000..2c3b8c772 --- /dev/null +++ b/spec/voice/secret_box_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'discordrb/voice/sodium' + +describe Discordrb::Voice::SecretBox do + def rand_bytes(size) + bytes = Array.new(size) { rand(256) } + bytes.pack('C*') + end + + it 'encrypts round trip' do + key = rand_bytes(described_class::KEY_LENGTH) + nonce = rand_bytes(described_class::NONCE_BYTES) + message = rand_bytes(20) + + secret_box = described_class.new(key) + ct = secret_box.box(nonce, message) + pt = secret_box.open(nonce, ct) + expect(pt).to eq message + end + + it 'raises on invalid key length' do + key = rand_bytes(described_class::KEY_LENGTH - 1) + expect { described_class.new(key) }.to raise_error(described_class::LengthError) + end + + describe '#box' do + it 'raises on invalid nonce length' do + key = rand_bytes(described_class::KEY_LENGTH) + nonce = rand_bytes(described_class::NONCE_BYTES - 1) + expect { described_class.new(key).box(nonce, '') }.to raise_error(described_class::LengthError) + end + end + + describe '#open' do + it 'raises on invalid nonce length' do + key = rand_bytes(described_class::KEY_LENGTH) + nonce = rand_bytes(described_class::NONCE_BYTES - 1) + expect { described_class.new(key).open(nonce, '') }.to raise_error(described_class::LengthError) + end + end +end diff --git a/spec/webhooks_spec.rb b/spec/webhooks_spec.rb index df5966817..b63c60c23 100644 --- a/spec/webhooks_spec.rb +++ b/spec/webhooks_spec.rb @@ -1,26 +1,26 @@ # frozen_string_literal: true +require 'base64' require 'securerandom' require 'discordrb/webhooks' describe Discordrb::Webhooks do describe Discordrb::Webhooks::Builder do - it 'should be able to add embeds' do - builder = Discordrb::Webhooks::Builder.new + it 'is able to add embeds' do + builder = described_class.new embed = builder.add_embed do |e| e.title = 'a' e.image = Discordrb::Webhooks::EmbedImage.new(url: 'https://example.com/image.png') end - expect(builder.embeds.length).to eq 1 expect(builder.embeds.first).to eq embed end end describe Discordrb::Webhooks::Embed do - it 'should be able to have fields added' do - embed = Discordrb::Webhooks::Embed.new + it 'is able to have fields added' do + embed = described_class.new embed.add_field(name: 'a', value: 'b', inline: true) @@ -28,54 +28,58 @@ end describe '#colour=' do - it 'should accept colours in decimal format' do - embed = Discordrb::Webhooks::Embed.new + it 'accepts colours in decimal format' do + embed = described_class.new colour = 1234 embed.colour = colour expect(embed.colour).to eq colour end - it 'should raise if the colour value is too high' do - embed = Discordrb::Webhooks::Embed.new + it 'raises if the colour value is too high' do + embed = described_class.new colour = 100_000_000 expect { embed.colour = colour }.to raise_error(ArgumentError) end - it 'should accept colours in hex format' do - embed = Discordrb::Webhooks::Embed.new + it 'accepts colours in hex format' do + embed = described_class.new colour = '162a3f' embed.colour = colour expect(embed.colour).to eq 1_452_607 end - it 'should accept colours in hex format with a # in front' do - embed = Discordrb::Webhooks::Embed.new + it 'accepts colours in hex format with a # in front' do + embed = described_class.new colour = '#162a3f' embed.colour = colour expect(embed.colour).to eq 1_452_607 end - it 'should accept colours as a RGB tuple' do - embed = Discordrb::Webhooks::Embed.new + it 'accepts colours as an RGB tuple' do + embed = described_class.new colour = [22, 42, 63] embed.colour = colour expect(embed.colour).to eq 1_452_607 end - it 'should raise if a RGB tuple is of the wrong size' do - embed = Discordrb::Webhooks::Embed.new + shared_examples 'should raise if a RGB tuple is of the wrong size' do |tuple| + it "raises an error for tuple #{tuple}" do + embed = described_class.new - expect { embed.colour = [0, 1] }.to raise_error(ArgumentError) - expect { embed.colour = [0, 1, 2, 3] }.to raise_error(ArgumentError) + expect { embed.colour = tuple }.to raise_error(ArgumentError) + end end - it 'should raise if a RGB tuple results in a too large value' do - embed = Discordrb::Webhooks::Embed.new + include_examples 'should raise if a RGB tuple is of the wrong size', [0, 1] + include_examples 'should raise if a RGB tuple is of the wrong size', [0, 1, 2, 3] + + it 'raises if an RGB tuple results in a too large value' do + embed = described_class.new expect { embed.colour = [2000, 1, 2] }.to raise_error(ArgumentError) end @@ -83,14 +87,14 @@ end describe Discordrb::Webhooks::Client do - let(:id) { SecureRandom.bytes(8) } - let(:token) { SecureRandom.bytes(24) } - let(:provided_url) { instance_double(String) } + subject(:client) { described_class.new(url: provided_url) } - subject { described_class.new(url: provided_url) } + let(:provided_url) { instance_double(String, 'provided url') } describe '#initialize' do it 'generates a url from id and token' do + id = SecureRandom.bytes(8) + token = SecureRandom.bytes(24) client = described_class.new(id: id, token: token) url = client.instance_variable_get(:@url) @@ -98,7 +102,6 @@ end it 'takes a provided url' do - client = described_class.new(url: provided_url) url = client.instance_variable_get(:@url) expect(url).to be provided_url @@ -110,40 +113,33 @@ let(:default_builder) { instance_double(Discordrb::Webhooks::Builder, to_json_hash: json_hash) } before do - allow(subject).to receive(:post_json).with(any_args) - allow(subject).to receive(:post_multipart).with(any_args) - allow(default_builder).to receive(:file).and_return(nil) + allow(client).to receive_messages(post_json: nil, post_multipart: nil) + allow(default_builder).to receive(:file) end it 'takes a default builder' do - expect { |b| subject.execute(default_builder, &b) }.to yield_with_args(default_builder, instance_of(Discordrb::Webhooks::View)) + expect { |b| client.execute(default_builder, &b) }.to yield_with_args(default_builder, instance_of(Discordrb::Webhooks::View)) end - context 'when a builder is not provided' do - it 'creates a new builder if none is provided' do - expect { |b| subject.execute(&b) }.to yield_with_args( - instance_of(Discordrb::Webhooks::Builder), - instance_of(Discordrb::Webhooks::View) - ) - end + it 'creates a new builder if none is provided' do + expect { |b| client.execute(&b) }.to yield_with_args( + instance_of(Discordrb::Webhooks::Builder), + instance_of(Discordrb::Webhooks::View) + ) end - context 'when a file is provided' do - it 'POSTs multipart data' do - allow(default_builder).to receive(:file).and_return(true) + it 'POSTs multipart data when a file is provided' do + allow(default_builder).to receive(:file).and_return(true) - subject.execute(default_builder) + client.execute(default_builder) - expect(subject).to have_received(:post_multipart).with(default_builder, any_args) - end + expect(client).to have_received(:post_multipart).with(default_builder, any_args) end - context 'when a file is not provided' do - it 'POSTs json data' do - subject.execute(default_builder) + it 'POSTs json data when a file is not provided' do + client.execute(default_builder) - expect(subject).to have_received(:post_json).with(default_builder, any_args) - end + expect(client).to have_received(:post_json).with(default_builder, any_args) end end @@ -155,7 +151,7 @@ end it 'sends a PATCH request to the URL' do - subject.modify + client.modify expect(RestClient).to have_received(:patch).with(provided_url, anything, content_type: :json) end @@ -169,7 +165,7 @@ it 'sends a DELETE request to the URL' do reason = instance_double(String) - subject.delete(reason: reason) + client.delete(reason: reason) expect(RestClient).to have_received(:delete).with(provided_url, 'X-Audit-Log-Reason': reason) end @@ -185,36 +181,35 @@ end it 'creates a new builder if one is not provided' do - expect { |b| subject.edit_message(message_id, &b) }.to yield_with_args(instance_of(Discordrb::Webhooks::Builder)) + expect { |b| client.edit_message(message_id, &b) }.to yield_with_args(instance_of(Discordrb::Webhooks::Builder)) end it 'uses the provided builder' do - expect { |b| subject.edit_message(message_id, builder: default_builder, &b) }.to yield_with_args(default_builder) + expect { |b| client.edit_message(message_id, builder: default_builder, &b) }.to yield_with_args(default_builder) end it 'sends a PATCH request to the message URL' do - url = subject.instance_variable_get(:@url) + url = client.instance_variable_get(:@url) - subject.edit_message(message_id) + client.edit_message(message_id) expect(RestClient).to have_received(:patch).with("#{url}/messages/#{message_id}", instance_of(String), content_type: :json) end end describe '#delete_message' do - let(:base_url) { 'url' } - let(:message_id) { SecureRandom.bytes(8) } + # subject(:client) { described_class.new(url: base_url) } - subject { described_class.new(url: base_url) } + let(:message_id) { SecureRandom.bytes(8) } before do allow(RestClient).to receive(:delete).with(any_args) end it 'sends a DELETE request to the message URL' do - subject.delete_message(message_id) + client.delete_message(message_id) - expect(RestClient).to have_received(:delete).with("#{base_url}/messages/#{message_id}") + expect(RestClient).to have_received(:delete).with("#{provided_url}/messages/#{message_id}") end end @@ -227,37 +222,38 @@ end it 'makes a POST request with JSON data' do - subject.__send__(:post_json, builder, [], false) + client.__send__(:post_json, builder, [], false) expect(RestClient).to have_received(:post).with(provided_url, builder.to_json_hash.merge({ components: [] }).to_json, content_type: :json) end it 'waits when wait=true' do - subject.__send__(:post_json, builder, [], true) + client.__send__(:post_json, builder, [], true) expect(provided_url).to have_received(:+).with('?wait=true') end end describe '#post_multipart' do - let(:post_data) { instance_double(Hash) } let(:multipart_hash) { instance_double(Hash) } let(:builder) { instance_double(Discordrb::Webhooks::Builder, to_multipart_hash: multipart_hash) } before do allow(RestClient).to receive(:post).with(any_args) allow(provided_url).to receive(:+).with(anything).and_return(provided_url) - allow(multipart_hash).to receive(:merge).with(instance_of(Hash)).and_return(post_data) end it 'makes a POST request with multipart data' do - subject.__send__(:post_multipart, builder, [], false) + post_data = instance_double(Hash) + allow(multipart_hash).to receive(:merge).with(instance_of(Hash)).and_return(post_data) + client.__send__(:post_multipart, builder, [], false) expect(RestClient).to have_received(:post).with(provided_url, post_data) end it 'waits for a response when wait=true' do - subject.__send__(:post_multipart, builder, [], true) + allow(multipart_hash).to receive(:merge) + client.__send__(:post_multipart, builder, [], true) expect(provided_url).to have_received(:+).with('?wait=true') end @@ -267,11 +263,11 @@ let(:data) { SecureRandom.bytes(24) } it 'makes no changes if the argument does not respond to read' do - expect(subject.__send__(:avatarise, data)).to be data + expect(client.__send__(:avatarise, data)).to be data end it 'returns multipart data if the argument responds to read' do - encoded = subject.__send__(:avatarise, StringIO.new(data)) + encoded = client.__send__(:avatarise, StringIO.new(data)) expect(encoded).to eq "data:image/jpg;base64,#{Base64.strict_encode64(data)}" end end