about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2017-12-04 11:07:01 -0600
committerDavid Yip <yipdw@member.fsf.org>2017-12-04 11:07:01 -0600
commitd9800a5647cbc57db7679094b2271f8eb5ec328b (patch)
treef9210c465de5f9d80e294d9ffa8536f98f9c466e /spec
parent1c74ede69e7a9916c19da6f05daa215231eba81c (diff)
parentf2f2f1032082d6212771bd0307136484f671d37e (diff)
Merge branch 'gs-master' into glitch-theme
Diffstat (limited to 'spec')
-rw-r--r--spec/fabricators/admin_action_log_fabricator.rb5
-rw-r--r--spec/fabricators/invite_fabricator.rb6
-rw-r--r--spec/lib/activitypub/activity/delete_spec.rb13
-rw-r--r--spec/lib/feed_manager_spec.rb16
-rw-r--r--spec/lib/settings/extend_spec.rb16
-rw-r--r--spec/models/admin/action_log_spec.rb5
-rw-r--r--spec/models/concerns/account_interactions_spec.rb592
-rw-r--r--spec/models/concerns/remotable_spec.rb205
-rw-r--r--spec/models/concerns/streamable_spec.rb63
-rw-r--r--spec/models/glitch/keyword_mute_spec.rb93
-rw-r--r--spec/models/invite_spec.rb30
-rw-r--r--spec/models/notification_spec.rb31
-rw-r--r--spec/models/status_spec.rb27
-rw-r--r--spec/models/user_spec.rb43
-rw-r--r--spec/presenters/account_relationships_presenter_spec.rb82
-rw-r--r--spec/services/activitypub/fetch_remote_status_service_spec.rb36
-rw-r--r--spec/services/notify_service_spec.rb20
-rw-r--r--spec/services/process_mentions_service_spec.rb21
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