diff options
Diffstat (limited to 'spec')
-rw-r--r-- | spec/fabricators/admin_action_log_fabricator.rb | 5 | ||||
-rw-r--r-- | spec/fabricators/invite_fabricator.rb | 6 | ||||
-rw-r--r-- | spec/lib/activitypub/activity/delete_spec.rb | 13 | ||||
-rw-r--r-- | spec/lib/feed_manager_spec.rb | 16 | ||||
-rw-r--r-- | spec/lib/settings/extend_spec.rb | 16 | ||||
-rw-r--r-- | spec/models/admin/action_log_spec.rb | 5 | ||||
-rw-r--r-- | spec/models/concerns/account_interactions_spec.rb | 592 | ||||
-rw-r--r-- | spec/models/concerns/remotable_spec.rb | 205 | ||||
-rw-r--r-- | spec/models/concerns/streamable_spec.rb | 63 | ||||
-rw-r--r-- | spec/models/glitch/keyword_mute_spec.rb | 93 | ||||
-rw-r--r-- | spec/models/invite_spec.rb | 30 | ||||
-rw-r--r-- | spec/models/notification_spec.rb | 31 | ||||
-rw-r--r-- | spec/models/status_spec.rb | 27 | ||||
-rw-r--r-- | spec/models/user_spec.rb | 43 | ||||
-rw-r--r-- | spec/presenters/account_relationships_presenter_spec.rb | 82 | ||||
-rw-r--r-- | spec/services/activitypub/fetch_remote_status_service_spec.rb | 36 | ||||
-rw-r--r-- | spec/services/notify_service_spec.rb | 20 | ||||
-rw-r--r-- | spec/services/process_mentions_service_spec.rb | 21 |
18 files changed, 1260 insertions, 44 deletions
diff --git a/spec/fabricators/admin_action_log_fabricator.rb b/spec/fabricators/admin_action_log_fabricator.rb new file mode 100644 index 000000000..2f44e953d --- /dev/null +++ b/spec/fabricators/admin_action_log_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator('Admin::ActionLog') do + account nil + action "MyString" + target nil +end diff --git a/spec/fabricators/invite_fabricator.rb b/spec/fabricators/invite_fabricator.rb new file mode 100644 index 000000000..62b9b3904 --- /dev/null +++ b/spec/fabricators/invite_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:invite) do + user + expires_at nil + max_uses nil + uses 0 +end diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb index 38254e31c..37b93ecf7 100644 --- a/spec/lib/activitypub/activity/delete_spec.rb +++ b/spec/lib/activitypub/activity/delete_spec.rb @@ -1,8 +1,8 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Delete do - let(:sender) { Fabricate(:account, domain: 'example.com') } - let(:status) { Fabricate(:status, account: sender, uri: 'foobar') } + let(:sender) { Fabricate(:account, domain: 'example.com') } + let(:status) { Fabricate(:status, account: sender, uri: 'foobar') } let(:json) do { @@ -30,13 +30,13 @@ RSpec.describe ActivityPub::Activity::Delete do context 'when the status has been reblogged' do describe '#perform' do subject { described_class.new(json, sender) } - let(:reblogger) { Fabricate(:account) } - let(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } + let!(:reblogger) { Fabricate(:account) } + let!(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } + let!(:reblog) { Fabricate(:status, account: reblogger, reblog: status) } before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200) follower.follow!(reblogger) - Fabricate(:status, account: reblogger, reblog: status) subject.perform end @@ -45,8 +45,7 @@ RSpec.describe ActivityPub::Activity::Delete do end it 'sends delete activity to followers of rebloggers' do - # one for Delete original post, and one for Undo reblog (normal delivery) - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once end end end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index ba96b6e7e..f87ef383a 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -164,6 +164,22 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true end + + it 'returns true for a status with a tag that matches a muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: 'jorts') + status = Fabricate(:status, account: bob) + status.tags << Fabricate(:tag, name: 'jorts') + + expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true + end + + it 'returns true for a status with a tag that matches an octothorpe-prefixed muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: '#jorts') + status = Fabricate(:status, account: bob) + status.tags << Fabricate(:tag, name: 'jorts') + + expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true + end end context 'for mentions feed' do diff --git a/spec/lib/settings/extend_spec.rb b/spec/lib/settings/extend_spec.rb new file mode 100644 index 000000000..83ced4230 --- /dev/null +++ b/spec/lib/settings/extend_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Settings::Extend do + class User + include Settings::Extend + end + + describe '#settings' do + it 'sets @settings as an instance of Settings::ScopedSettings' do + user = Fabricate(:user) + expect(user.settings).to be_kind_of Settings::ScopedSettings + end + end +end diff --git a/spec/models/admin/action_log_spec.rb b/spec/models/admin/action_log_spec.rb new file mode 100644 index 000000000..59206a36b --- /dev/null +++ b/spec/models/admin/action_log_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Admin::ActionLog, type: :model do + +end diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 1e238e27c..95bf9561d 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -1,6 +1,561 @@ require 'rails_helper' describe AccountInteractions do + let(:account) { Fabricate(:account, username: 'account') } + let(:account_id) { account.id } + let(:account_ids) { [account_id] } + let(:target_account) { Fabricate(:account, username: 'target') } + let(:target_account_id) { target_account.id } + let(:target_account_ids) { [target_account_id] } + + describe '.following_map' do + subject { Account.following_map(target_account_ids, account_id) } + + context 'account with Follow' do + it 'returns { target_account_id => { reblogs: true } }' do + Fabricate(:follow, account: account, target_account: target_account) + is_expected.to eq(target_account_id => { reblogs: true }) + end + end + + context 'account with Follow but with reblogs disabled' do + it 'returns { target_account_id => { reblogs: false } }' do + Fabricate(:follow, account: account, target_account: target_account, show_reblogs: false) + is_expected.to eq(target_account_id => { reblogs: false }) + end + end + + context 'account without Follow' do + it 'returns {}' do + is_expected.to eq({}) + end + end + end + + describe '.followed_by_map' do + subject { Account.followed_by_map(target_account_ids, account_id) } + + context 'account with Follow' do + it 'returns { target_account_id => true }' do + Fabricate(:follow, account: target_account, target_account: account) + is_expected.to eq(target_account_id => true) + end + end + + context 'account without Follow' do + it 'returns {}' do + is_expected.to eq({}) + end + end + end + + describe '.blocking_map' do + subject { Account.blocking_map(target_account_ids, account_id) } + + context 'account with Block' do + it 'returns { target_account_id => true }' do + Fabricate(:block, account: account, target_account: target_account) + is_expected.to eq(target_account_id => true) + end + end + + context 'account without Block' do + it 'returns {}' do + is_expected.to eq({}) + end + end + end + + describe '.muting_map' do + subject { Account.muting_map(target_account_ids, account_id) } + + context 'account with Mute' do + before do + Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide) + end + + context 'if Mute#hide_notifications?' do + let(:hide) { true } + + it 'returns { target_account_id => { notifications: true } }' do + is_expected.to eq(target_account_id => { notifications: true }) + end + end + + context 'unless Mute#hide_notifications?' do + let(:hide) { false } + + it 'returns { target_account_id => { notifications: false } }' do + is_expected.to eq(target_account_id => { notifications: false }) + end + end + end + + context 'account without Mute' do + it 'returns {}' do + is_expected.to eq({}) + end + end + end + + describe '#follow!' do + it 'creates and returns Follow' do + expect do + expect(account.follow!(target_account)).to be_kind_of Follow + end.to change { account.following.count }.by 1 + end + end + + describe '#block' do + it 'creates and returns Block' do + expect do + expect(account.block!(target_account)).to be_kind_of Block + end.to change { account.block_relationships.count }.by 1 + end + end + + describe '#mute!' do + context 'Mute does not exist yet' do + context 'arg :notifications is nil' do + let(:arg_notifications) { nil } + + it 'creates Mute, and returns nil' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be nil + end.to change { account.mute_relationships.count }.by 1 + end + end + + context 'arg :notifications is false' do + let(:arg_notifications) { false } + + it 'creates Mute, and returns nil' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be nil + end.to change { account.mute_relationships.count }.by 1 + end + end + + context 'arg :notifications is true' do + let(:arg_notifications) { true } + + it 'creates Mute, and returns nil' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be nil + end.to change { account.mute_relationships.count }.by 1 + end + end + end + + context 'Mute already exists' do + before do + account.mute_relationships << mute + end + + let(:mute) do + Fabricate(:mute, + account: account, + target_account: target_account, + hide_notifications: hide_notifications) + end + + context 'mute.hide_notifications is true' do + let(:hide_notifications) { true } + + context 'arg :notifications is nil' do + let(:arg_notifications) { nil } + + it 'returns nil without updating mute.hide_notifications' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be nil + mute = account.mute_relationships.find_by(target_account: target_account) + expect(mute.hide_notifications?).to be true + end + end + end + + context 'arg :notifications is false' do + let(:arg_notifications) { false } + + it 'returns true, and updates mute.hide_notifications false' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be true + mute = account.mute_relationships.find_by(target_account: target_account) + expect(mute.hide_notifications?).to be false + end + end + end + + context 'arg :notifications is true' do + let(:arg_notifications) { true } + + it 'returns nil without updating mute.hide_notifications' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be nil + mute = account.mute_relationships.find_by(target_account: target_account) + expect(mute.hide_notifications?).to be true + end + end + end + end + + context 'mute.hide_notifications is false' do + let(:hide_notifications) { false } + + context 'arg :notifications is nil' do + let(:arg_notifications) { nil } + + it 'returns true, and updates mute.hide_notifications true' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be true + mute = account.mute_relationships.find_by(target_account: target_account) + expect(mute.hide_notifications?).to be true + end + end + end + + context 'arg :notifications is false' do + let(:arg_notifications) { false } + + it 'returns nil without updating mute.hide_notifications' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be nil + mute = account.mute_relationships.find_by(target_account: target_account) + expect(mute.hide_notifications?).to be false + end + end + end + + context 'arg :notifications is true' do + let(:arg_notifications) { true } + + it 'returns true, and updates mute.hide_notifications true' do + expect do + expect(account.mute!(target_account, notifications: arg_notifications)).to be true + mute = account.mute_relationships.find_by(target_account: target_account) + expect(mute.hide_notifications?).to be true + end + end + end + end + end + end + + describe '#mute_conversation!' do + let(:conversation) { Fabricate(:conversation) } + + subject { account.mute_conversation!(conversation) } + + it 'creates and returns ConversationMute' do + expect do + is_expected.to be_kind_of ConversationMute + end.to change { account.conversation_mutes.count }.by 1 + end + end + + describe '#block_domain!' do + let(:domain_block) { Fabricate(:domain_block) } + + subject { account.block_domain!(domain_block) } + + it 'creates and returns AccountDomainBlock' do + expect do + is_expected.to be_kind_of AccountDomainBlock + end.to change { account.domain_blocks.count }.by 1 + end + end + + describe '#unfollow!' do + subject { account.unfollow!(target_account) } + + context 'following target_account' do + it 'returns destroyed Follow' do + account.active_relationships.create(target_account: target_account) + is_expected.to be_kind_of Follow + expect(subject).to be_destroyed + end + end + + context 'not following target_account' do + it 'returns nil' do + is_expected.to be_nil + end + end + end + + describe '#unblock!' do + subject { account.unblock!(target_account) } + + context 'blocking target_account' do + it 'returns destroyed Block' do + account.block_relationships.create(target_account: target_account) + is_expected.to be_kind_of Block + expect(subject).to be_destroyed + end + end + + context 'not blocking target_account' do + it 'returns nil' do + is_expected.to be_nil + end + end + end + + describe '#unmute!' do + subject { account.unmute!(target_account) } + + context 'muting target_account' do + it 'returns destroyed Mute' do + account.mute_relationships.create(target_account: target_account) + is_expected.to be_kind_of Mute + expect(subject).to be_destroyed + end + end + + context 'not muting target_account' do + it 'returns nil' do + is_expected.to be_nil + end + end + end + + describe '#unmute_conversation!' do + let(:conversation) { Fabricate(:conversation) } + + subject { account.unmute_conversation!(conversation) } + + context 'muting the conversation' do + it 'returns destroyed ConversationMute' do + account.conversation_mutes.create(conversation: conversation) + is_expected.to be_kind_of ConversationMute + expect(subject).to be_destroyed + end + end + + context 'not muting the conversation' do + it 'returns nil' do + is_expected.to be nil + end + end + end + + describe '#unblock_domain!' do + let(:domain) { 'example.com' } + + subject { account.unblock_domain!(domain) } + + context 'blocking the domain' do + it 'returns destroyed AccountDomainBlock' do + account_domain_block = Fabricate(:account_domain_block, domain: domain) + account.domain_blocks << account_domain_block + is_expected.to be_kind_of AccountDomainBlock + expect(subject).to be_destroyed + end + end + + context 'unblocking the domain' do + it 'returns nil' do + is_expected.to be_nil + end + end + end + + describe '#following?' do + subject { account.following?(target_account) } + + context 'following target_account' do + it 'returns true' do + account.active_relationships.create(target_account: target_account) + is_expected.to be true + end + end + + context 'not following target_account' do + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#blocking?' do + subject { account.blocking?(target_account) } + + context 'blocking target_account' do + it 'returns true' do + account.block_relationships.create(target_account: target_account) + is_expected.to be true + end + end + + context 'not blocking target_account' do + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#domain_blocking?' do + let(:domain) { 'example.com' } + + subject { account.domain_blocking?(domain) } + + context 'blocking the domain' do + it' returns true' do + account_domain_block = Fabricate(:account_domain_block, domain: domain) + account.domain_blocks << account_domain_block + is_expected.to be true + end + end + + context 'not blocking the domain' do + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#muting?' do + subject { account.muting?(target_account) } + + context 'muting target_account' do + it 'returns true' do + mute = Fabricate(:mute, account: account, target_account: target_account) + account.mute_relationships << mute + is_expected.to be true + end + end + + context 'not muting target_account' do + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#muting_conversation?' do + let(:conversation) { Fabricate(:conversation) } + + subject { account.muting_conversation?(conversation) } + + context 'muting the conversation' do + it 'returns true' do + account.conversation_mutes.create(conversation: conversation) + is_expected.to be true + end + end + + context 'not muting the conversation' do + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#muting_notifications?' do + before do + mute = Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide) + account.mute_relationships << mute + end + + subject { account.muting_notifications?(target_account) } + + context 'muting notifications of target_account' do + let(:hide) { true } + + it 'returns true' do + is_expected.to be true + end + end + + context 'not muting notifications of target_account' do + let(:hide) { false } + + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#requested?' do + subject { account.requested?(target_account) } + + context 'requested by target_account' do + it 'returns true' do + Fabricate(:follow_request, account: account, target_account: target_account) + is_expected.to be true + end + end + + context 'not requested by target_account' do + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#favourited?' do + let(:status) { Fabricate(:status, account: account, favourites: favourites) } + + subject { account.favourited?(status) } + + context 'favorited' do + let(:favourites) { [Fabricate(:favourite, account: account)] } + + it 'returns true' do + is_expected.to be true + end + end + + context 'not favorited' do + let(:favourites) { [] } + + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#reblogged?' do + let(:status) { Fabricate(:status, account: account, reblogs: reblogs) } + + subject { account.reblogged?(status) } + + context 'reblogged' do + let(:reblogs) { [Fabricate(:status, account: account)] } + + it 'returns true' do + is_expected.to be true + end + end + + context 'not reblogged' do + let(:reblogs) { [] } + + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#pinned?' do + let(:status) { Fabricate(:status, account: account) } + + subject { account.pinned?(status) } + + context 'pinned' do + it 'returns true' do + Fabricate(:status_pin, account: account, status: status) + is_expected.to be true + end + end + + context 'not pinned' do + it 'returns false' do + is_expected.to be false + end + end + end + describe 'muting an account' do let(:me) { Fabricate(:account, username: 'Me') } let(:you) { Fabricate(:account, username: 'You') } @@ -72,4 +627,41 @@ describe AccountInteractions do end end end + + describe 'ignoring reblogs from an account' do + before do + @me = Fabricate(:account, username: 'Me') + @you = Fabricate(:account, username: 'You') + end + + context 'with the reblogs option unspecified' do + before do + @me.follow!(@you) + end + + it 'defaults to showing reblogs' do + expect(@me.muting_reblogs?(@you)).to be(false) + end + end + + context 'with the reblogs option set to false' do + before do + @me.follow!(@you, reblogs: false) + end + + it 'does mute reblogs' do + expect(@me.muting_reblogs?(@you)).to be(true) + end + end + + context 'with the reblogs option set to true' do + before do + @me.follow!(@you, reblogs: true) + end + + it 'does not mute reblogs' do + expect(@me.muting_reblogs?(@you)).to be(false) + end + end + end end diff --git a/spec/models/concerns/remotable_spec.rb b/spec/models/concerns/remotable_spec.rb new file mode 100644 index 000000000..0b2dad23f --- /dev/null +++ b/spec/models/concerns/remotable_spec.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Remotable do + class Foo + def initialize + @attrs = {} + end + + def [](arg) + @attrs[arg] + end + + def []=(arg1, arg2) + @attrs[arg1] = arg2 + end + + def hoge=(arg); end + + def hoge_file_name=(arg); end + + def has_attribute?(arg); end + + def self.attachment_definitions + { hoge: nil } + end + end + + context 'Remotable module is included' do + before do + class Foo; include Remotable; end + end + + let(:attribute_name) { "#{hoge}_remote_url".to_sym } + let(:code) { 200 } + let(:file) { 'filename="foo.txt"' } + let(:foo) { Foo.new } + let(:headers) { { 'content-disposition' => file } } + let(:hoge) { :hoge } + let(:url) { 'https://google.com' } + + let(:request) do + stub_request(:get, url) + .to_return(status: code, headers: headers) + end + + it 'defines a method #hoge_remote_url=' do + expect(foo).to respond_to(:hoge_remote_url=) + end + + it 'defines a method #reset_hoge!' do + expect(foo).to respond_to(:reset_hoge!) + end + + describe '#hoge_remote_url' do + before do + request + end + + it 'always returns arg' do + [nil, '', [], {}].each do |arg| + expect(foo.hoge_remote_url = arg).to be arg + end + end + + context 'Addressable::URI::InvalidURIError raised' do + it 'makes no request' do + allow(Addressable::URI).to receive_message_chain(:parse, :normalize) + .with(url).with(no_args).and_raise(Addressable::URI::InvalidURIError) + + foo.hoge_remote_url = url + expect(request).not_to have_been_requested + end + end + + context 'scheme is neither http nor https' do + let(:url) { 'ftp://google.com' } + + it 'makes no request' do + foo.hoge_remote_url = url + expect(request).not_to have_been_requested + end + end + + context 'parsed_url.host is empty' do + it 'makes no request' do + parsed_url = double(scheme: 'https', host: double(empty?: true)) + allow(Addressable::URI).to receive_message_chain(:parse, :normalize) + .with(url).with(no_args).and_return(parsed_url) + + foo.hoge_remote_url = url + expect(request).not_to have_been_requested + end + end + + context 'foo[attribute_name] == url' do + it 'makes no request' do + allow(foo).to receive(:[]).with(attribute_name).and_return(url) + + foo.hoge_remote_url = url + expect(request).not_to have_been_requested + end + end + + context "scheme is https, parsed_url.host isn't empty, and foo[attribute_name] != url" do + it 'makes a request' do + foo.hoge_remote_url = url + expect(request).to have_been_requested + end + + context 'response.code != 200' do + let(:code) { 500 } + + it 'calls not send' do + expect(foo).not_to receive(:send).with("#{hoge}=", any_args) + expect(foo).not_to receive(:send).with("#{hoge}_file_name=", any_args) + foo.hoge_remote_url = url + end + end + + context 'response.code == 200' do + let(:code) { 200 } + + context 'response contains headers["content-disposition"]' do + let(:file) { 'filename="foo.txt"' } + let(:headers) { { 'content-disposition' => file } } + + it 'calls send' do + string_io = StringIO.new('') + extname = '.txt' + basename = '0123456789abcdef' + + allow(SecureRandom).to receive(:hex).and_return(basename) + allow(StringIO).to receive(:new).with(anything).and_return(string_io) + + expect(foo).to receive(:send).with("#{hoge}=", string_io) + expect(foo).to receive(:send).with("#{hoge}_file_name=", basename + extname) + foo.hoge_remote_url = url + end + end + + context 'if has_attribute?' do + it 'calls foo[attribute_name] = url' do + allow(foo).to receive(:has_attribute?).with(attribute_name).and_return(true) + expect(foo).to receive('[]=').with(attribute_name, url) + foo.hoge_remote_url = url + end + end + + context 'unless has_attribute?' do + it 'calls not foo[attribute_name] = url' do + allow(foo).to receive(:has_attribute?) + .with(attribute_name).and_return(false) + expect(foo).not_to receive('[]=').with(attribute_name, url) + foo.hoge_remote_url = url + end + end + end + + context 'an error raised during the request' do + let(:request) { stub_request(:get, url).to_raise(error_class) } + + error_classes = [ + HTTP::TimeoutError, + HTTP::ConnectionError, + OpenSSL::SSL::SSLError, + Paperclip::Errors::NotIdentifiedByImageMagickError, + Addressable::URI::InvalidURIError, + ] + + error_classes.each do |error_class| + let(:error_class) { error_class } + + it 'calls Rails.logger.debug' do + expect(Rails.logger).to receive(:debug).with(/^Error fetching remote #{hoge}: /) + foo.hoge_remote_url = url + end + end + end + end + end + + describe '#reset_hoge!' do + context 'if url.blank?' do + it 'returns nil, without clearing foo[attribute_name] and calling #hoge_remote_url=' do + url = nil + expect(foo).not_to receive(:send).with(:hoge_remote_url=, url) + foo[attribute_name] = url + expect(foo.reset_hoge!).to be_nil + expect(foo[attribute_name]).to be_nil + end + end + + context 'unless url.blank?' do + it 'clears foo[attribute_name] and calls #hoge_remote_url=' do + foo[attribute_name] = url + expect(foo).to receive(:send).with(:hoge_remote_url=, url) + foo.reset_hoge! + expect(foo[attribute_name]).to be '' + end + end + end + end +end diff --git a/spec/models/concerns/streamable_spec.rb b/spec/models/concerns/streamable_spec.rb new file mode 100644 index 000000000..b5f2d5192 --- /dev/null +++ b/spec/models/concerns/streamable_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Streamable do + class Parent + def title; end + + def target; end + + def thread; end + + def self.has_one(*); end + + def self.after_create; end + end + + class Child < Parent + include Streamable + end + + child = Child.new + + describe '#title' do + it 'calls Parent#title' do + expect_any_instance_of(Parent).to receive(:title) + child.title + end + end + + describe '#content' do + it 'calls #title' do + expect_any_instance_of(Parent).to receive(:title) + child.content + end + end + + describe '#target' do + it 'calls Parent#target' do + expect_any_instance_of(Parent).to receive(:target) + child.target + end + end + + describe '#object_type' do + it 'returns :activity' do + expect(child.object_type).to eq :activity + end + end + + describe '#thread' do + it 'calls Parent#thread' do + expect_any_instance_of(Parent).to receive(:thread) + child.thread + end + end + + describe '#hidden?' do + it 'returns false' do + expect(child.hidden?).to be false + end + end +end diff --git a/spec/models/glitch/keyword_mute_spec.rb b/spec/models/glitch/keyword_mute_spec.rb index 9685c6493..0ffc7b18f 100644 --- a/spec/models/glitch/keyword_mute_spec.rb +++ b/spec/models/glitch/keyword_mute_spec.rb @@ -4,8 +4,8 @@ RSpec.describe Glitch::KeywordMute, type: :model do let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) } let(:bob) { Fabricate(:account, username: 'bob').tap(&:save!) } - describe '.matcher_for' do - let(:matcher) { Glitch::KeywordMute.matcher_for(alice) } + describe '.text_matcher_for' do + let(:matcher) { Glitch::KeywordMute.text_matcher_for(alice.id) } describe 'with no mutes' do before do @@ -13,7 +13,7 @@ RSpec.describe Glitch::KeywordMute, type: :model do end it 'does not match' do - expect(matcher =~ 'This is a hot take').to be_falsy + expect(matcher.matches?('This is a hot take')).to be_falsy end end @@ -21,75 +21,136 @@ RSpec.describe Glitch::KeywordMute, type: :model do it 'does not match keywords set by a different account' do Glitch::KeywordMute.create!(account: bob, keyword: 'take') - expect(matcher =~ 'This is a hot take').to be_falsy + expect(matcher.matches?('This is a hot take')).to be_falsy end it 'does not match if no keywords match the status text' do Glitch::KeywordMute.create!(account: alice, keyword: 'cold') - expect(matcher =~ 'This is a hot take').to be_falsy + expect(matcher.matches?('This is a hot take')).to be_falsy end it 'considers word boundaries when matching' do Glitch::KeywordMute.create!(account: alice, keyword: 'bob', whole_word: true) - expect(matcher =~ 'bobcats').to be_falsy + expect(matcher.matches?('bobcats')).to be_falsy end it 'matches substrings if whole_word is false' do Glitch::KeywordMute.create!(account: alice, keyword: 'take', whole_word: false) - expect(matcher =~ 'This is a shiitake mushroom').to be_truthy + expect(matcher.matches?('This is a shiitake mushroom')).to be_truthy end it 'matches keywords at the beginning of the text' do Glitch::KeywordMute.create!(account: alice, keyword: 'take') - expect(matcher =~ 'Take this').to be_truthy + expect(matcher.matches?('Take this')).to be_truthy end it 'matches keywords at the end of the text' do Glitch::KeywordMute.create!(account: alice, keyword: 'take') - expect(matcher =~ 'This is a hot take').to be_truthy + expect(matcher.matches?('This is a hot take')).to be_truthy end it 'matches if at least one keyword case-insensitively matches the text' do Glitch::KeywordMute.create!(account: alice, keyword: 'hot') - expect(matcher =~ 'This is a HOT take').to be_truthy + expect(matcher.matches?('This is a HOT take')).to be_truthy end it 'maintains case-insensitivity when combining keywords into a single matcher' do Glitch::KeywordMute.create!(account: alice, keyword: 'hot') Glitch::KeywordMute.create!(account: alice, keyword: 'cold') - expect(matcher =~ 'This is a HOT take').to be_truthy + expect(matcher.matches?('This is a HOT take')).to be_truthy end it 'matches keywords surrounded by non-alphanumeric ornamentation' do Glitch::KeywordMute.create!(account: alice, keyword: 'hot') - expect(matcher =~ '(hot take)').to be_truthy + expect(matcher.matches?('(hot take)')).to be_truthy end it 'escapes metacharacters in keywords' do Glitch::KeywordMute.create!(account: alice, keyword: '(hot take)') - expect(matcher =~ '(hot take)').to be_truthy + expect(matcher.matches?('(hot take)')).to be_truthy end it 'uses case-folding rules appropriate for more than just English' do Glitch::KeywordMute.create!(account: alice, keyword: 'großeltern') - expect(matcher =~ 'besuch der grosseltern').to be_truthy + expect(matcher.matches?('besuch der grosseltern')).to be_truthy end it 'matches keywords that are composed of multiple words' do Glitch::KeywordMute.create!(account: alice, keyword: 'a shiitake') - expect(matcher =~ 'This is a shiitake').to be_truthy - expect(matcher =~ 'This is shiitake').to_not be_truthy + expect(matcher.matches?('This is a shiitake')).to be_truthy + expect(matcher.matches?('This is shiitake')).to_not be_truthy + end + end + end + + describe '.tag_matcher_for' do + let(:matcher) { Glitch::KeywordMute.tag_matcher_for(alice.id) } + let(:status) { Fabricate(:status) } + + describe 'with no mutes' do + before do + Glitch::KeywordMute.delete_all + end + + it 'does not match' do + status.tags << Fabricate(:tag, name: 'xyzzy') + + expect(matcher.matches?(status.tags)).to be false + end + end + + describe 'with mutes' do + it 'does not match keywords set by a different account' do + status.tags << Fabricate(:tag, name: 'xyzzy') + Glitch::KeywordMute.create!(account: bob, keyword: 'take') + + expect(matcher.matches?(status.tags)).to be false + end + + it 'matches #xyzzy when given the mute "#xyzzy"' do + status.tags << Fabricate(:tag, name: 'xyzzy') + Glitch::KeywordMute.create!(account: alice, keyword: '#xyzzy') + + expect(matcher.matches?(status.tags)).to be true + end + + it 'matches #thingiverse when given the non-whole-word mute "#thing"' do + status.tags << Fabricate(:tag, name: 'thingiverse') + Glitch::KeywordMute.create!(account: alice, keyword: '#thing', whole_word: false) + + expect(matcher.matches?(status.tags)).to be true + end + + it 'matches #hashtag when given the mute "##hashtag""' do + status.tags << Fabricate(:tag, name: 'hashtag') + Glitch::KeywordMute.create!(account: alice, keyword: '##hashtag') + + expect(matcher.matches?(status.tags)).to be true + end + + it 'matches #oatmeal when given the non-whole-word mute "oat"' do + status.tags << Fabricate(:tag, name: 'oatmeal') + Glitch::KeywordMute.create!(account: alice, keyword: 'oat', whole_word: false) + + expect(matcher.matches?(status.tags)).to be true + end + + it 'does not match #oatmeal when given the mute "#oat"' do + status.tags << Fabricate(:tag, name: 'oatmeal') + Glitch::KeywordMute.create!(account: alice, keyword: 'oat') + + expect(matcher.matches?(status.tags)).to be false end end end diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb new file mode 100644 index 000000000..0ba1dccb3 --- /dev/null +++ b/spec/models/invite_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +RSpec.describe Invite, type: :model do + describe '#valid_for_use?' do + it 'returns true when there are no limitations' do + invite = Invite.new(max_uses: nil, expires_at: nil) + expect(invite.valid_for_use?).to be true + end + + it 'returns true when not expired' do + invite = Invite.new(max_uses: nil, expires_at: 1.hour.from_now) + expect(invite.valid_for_use?).to be true + end + + it 'returns false when expired' do + invite = Invite.new(max_uses: nil, expires_at: 1.hour.ago) + expect(invite.valid_for_use?).to be false + end + + it 'returns true when uses still available' do + invite = Invite.new(max_uses: 250, uses: 249, expires_at: nil) + expect(invite.valid_for_use?).to be true + end + + it 'returns false when maximum uses reached' do + invite = Invite.new(max_uses: 250, uses: 250, expires_at: nil) + expect(invite.valid_for_use?).to be false + end + end +end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 763b1523f..8444c8f63 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -6,23 +6,18 @@ RSpec.describe Notification, type: :model do end describe '#target_status' do - before do - allow(notification).to receive(:type).and_return(type) - allow(notification).to receive(:activity).and_return(activity) - end - - let(:notification) { Fabricate(:notification) } - let(:status) { instance_double('Status') } - let(:favourite) { instance_double('Favourite') } - let(:mention) { instance_double('Mention') } + let(:notification) { Fabricate(:notification, activity_type: type, activity: activity) } + let(:status) { Fabricate(:status) } + let(:reblog) { Fabricate(:status, reblog: status) } + let(:favourite) { Fabricate(:favourite, status: status) } + let(:mention) { Fabricate(:mention, status: status) } context 'type is :reblog' do let(:type) { :reblog } - let(:activity) { status } + let(:activity) { reblog } - it 'calls activity.reblog' do - expect(activity).to receive(:reblog) - notification.target_status + it 'returns status' do + expect(notification.target_status).to eq status end end @@ -30,9 +25,8 @@ RSpec.describe Notification, type: :model do let(:type) { :favourite } let(:activity) { favourite } - it 'calls activity.status' do - expect(activity).to receive(:status) - notification.target_status + it 'returns status' do + expect(notification.target_status).to eq status end end @@ -40,9 +34,8 @@ RSpec.describe Notification, type: :model do let(:type) { :mention } let(:activity) { mention } - it 'calls activity.status' do - expect(activity).to receive(:status) - notification.target_status + it 'returns status' do + expect(notification.target_status).to eq status end end end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 89ad3adcf..c6701018e 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -83,8 +83,31 @@ RSpec.describe Status, type: :model do end describe '#title' do - it 'is a shorter version of the content' do - expect(subject.title).to be_a String + # rubocop:disable Style/InterpolationCheck + + let(:account) { subject.account } + + context 'if destroyed?' do + it 'returns "#{account.acct} deleted status"' do + subject.destroy! + expect(subject.title).to eq "#{account.acct} deleted status" + end + end + + context 'unless destroyed?' do + context 'if reblog?' do + it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do + reblog = subject.reblog = other + expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}" + end + end + + context 'unless reblog?' do + it 'returns "New status by #{account.acct}"' do + subject.reblog = nil + expect(subject.title).to eq "New status by #{account.acct}" + end + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 77a12c26d..5ed7ed88b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -273,4 +273,47 @@ RSpec.describe User, type: :model do expect(user.token_for_app(app)).to be_nil end end + + describe '#role' do + it 'returns admin for admin' do + user = User.new(admin: true) + expect(user.role).to eq 'admin' + end + + it 'returns moderator for moderator' do + user = User.new(moderator: true) + expect(user.role).to eq 'moderator' + end + + it 'returns user otherwise' do + user = User.new + expect(user.role).to eq 'user' + end + end + + describe '#role?' do + it 'returns false when invalid role requested' do + user = User.new(admin: true) + expect(user.role?('disabled')).to be false + end + + it 'returns true when exact role match' do + user = User.new + mod = User.new(moderator: true) + admin = User.new(admin: true) + + expect(user.role?('user')).to be true + expect(mod.role?('moderator')).to be true + expect(admin.role?('admin')).to be true + end + + it 'returns true when role higher than needed' do + mod = User.new(moderator: true) + admin = User.new(admin: true) + + expect(mod.role?('user')).to be true + expect(admin.role?('user')).to be true + expect(admin.role?('moderator')).to be true + end + end end diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb new file mode 100644 index 000000000..f8b048d38 --- /dev/null +++ b/spec/presenters/account_relationships_presenter_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AccountRelationshipsPresenter do + describe '.initialize' do + before do + allow(Account).to receive(:following_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:followed_by_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map) + end + + let(:presenter) { AccountRelationshipsPresenter.new(account_ids, current_account_id, options) } + let(:current_account_id) { Fabricate(:account).id } + let(:account_ids) { [Fabricate(:account).id] } + let(:default_map) { { 1 => true } } + + context 'options are not set' do + let(:options) { {} } + + it 'sets default maps' do + expect(presenter.following).to eq default_map + expect(presenter.followed_by).to eq default_map + expect(presenter.blocking).to eq default_map + expect(presenter.muting).to eq default_map + expect(presenter.requested).to eq default_map + expect(presenter.domain_blocking).to eq default_map + end + end + + context 'options[:following_map] is set' do + let(:options) { { following_map: { 2 => true } } } + + it 'sets @following merged with default_map and options[:following_map]' do + expect(presenter.following).to eq default_map.merge(options[:following_map]) + end + end + + context 'options[:followed_by_map] is set' do + let(:options) { { followed_by_map: { 3 => true } } } + + it 'sets @followed_by merged with default_map and options[:followed_by_map]' do + expect(presenter.followed_by).to eq default_map.merge(options[:followed_by_map]) + end + end + + context 'options[:blocking_map] is set' do + let(:options) { { blocking_map: { 4 => true } } } + + it 'sets @blocking merged with default_map and options[:blocking_map]' do + expect(presenter.blocking).to eq default_map.merge(options[:blocking_map]) + end + end + + context 'options[:muting_map] is set' do + let(:options) { { muting_map: { 5 => true } } } + + it 'sets @muting merged with default_map and options[:muting_map]' do + expect(presenter.muting).to eq default_map.merge(options[:muting_map]) + end + end + + context 'options[:requested_map] is set' do + let(:options) { { requested_map: { 6 => true } } } + + it 'sets @requested merged with default_map and options[:requested_map]' do + expect(presenter.requested).to eq default_map.merge(options[:requested_map]) + end + end + + context 'options[:domain_blocking_map] is set' do + let(:options) { { domain_blocking_map: { 7 => true } } } + + it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do + expect(presenter.domain_blocking).to eq default_map.merge(options[:domain_blocking_map]) + end + end + end +end diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 51f3fe3a1..ad26abc5b 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteStatusService do + include ActionView::Helpers::TextHelper + let(:sender) { Fabricate(:account) } let(:recipient) { Fabricate(:account) } let(:valid_domain) { Rails.configuration.x.local_domain } @@ -19,6 +21,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do describe '#call' do before do + stub_request(:head, 'https://example.com/watch?v=12345').to_return(status: 404, body: '') subject.call(object[:id], prefetched_body: Oj.dump(object)) end @@ -32,5 +35,38 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do expect(status.text).to eq 'Lorem ipsum' end end + + context 'with Video object' do + let(:object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: "https://#{valid_domain}/@foo/1234", + type: 'Video', + name: 'Nyan Cat 10 hours remix', + attributedTo: ActivityPub::TagManager.instance.uri_for(sender), + url: [ + { + type: 'Link', + mimeType: 'application/x-bittorrent', + href: 'https://example.com/12345.torrent', + }, + + { + type: 'Link', + mimeType: 'text/html', + href: 'https://example.com/watch?v=12345', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.url).to eq 'https://example.com/watch?v=12345' + expect(strip_tags(status.text)).to eq 'Nyan Cat 10 hours remix https://example.com/watch?v=12345' + end + end end end diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index a8ebc16b8..bb7601e76 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -101,6 +101,26 @@ RSpec.describe NotifyService do end end + describe 'reblogs' do + let(:status) { Fabricate(:status, account: Fabricate(:account)) } + let(:activity) { Fabricate(:status, account: sender, reblog: status) } + + it 'shows reblogs by default' do + recipient.follow!(sender) + is_expected.to change(Notification, :count) + end + + it 'shows reblogs when explicitly enabled' do + recipient.follow!(sender, reblogs: true) + is_expected.to change(Notification, :count) + end + + it 'hides reblogs when disabled' do + recipient.follow!(sender, reblogs: false) + is_expected.to_not change(Notification, :count) + end + end + context do let(:asshole) { Fabricate(:account, username: 'asshole') } let(:reply_to) { Fabricate(:status, account: asshole) } diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 09f8fa45b..19a8678f0 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -41,4 +41,25 @@ RSpec.describe ProcessMentionsService do expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once end end + + context 'Temporarily-unreachable ActivityPub user' do + let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) } + + subject { ProcessMentionsService.new } + + before do + stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404) + stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com").to_return(status: 500) + stub_request(:post, remote_user.inbox_url) + subject.call(status) + end + + it 'creates a mention' do + expect(remote_user.mentions.where(status: status).count).to eq 1 + end + + it 'sends activity to the inbox' do + expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once + end + end end |