about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/accounts_controller_spec.rb10
-rw-r--r--spec/controllers/api/v1/admin/accounts_controller_spec.rb35
-rw-r--r--spec/controllers/api/v2/admin/accounts_controller_spec.rb73
-rw-r--r--spec/controllers/settings/exports/bookmarks_controller_spec.rb (renamed from spec/controllers/settings/exports/bookmarks_controller_specs.rb)11
-rw-r--r--spec/lib/advanced_text_formatter_spec.rb282
-rw-r--r--spec/lib/emoji_formatter_spec.rb55
-rw-r--r--spec/lib/formatter_spec.rb638
-rw-r--r--spec/lib/html_aware_formatter_spec.rb44
-rw-r--r--spec/lib/link_details_extractor_spec.rb8
-rw-r--r--spec/lib/plain_text_formatter_spec.rb24
-rw-r--r--spec/lib/rss/serializer_spec.rb7
-rw-r--r--spec/lib/sanitize_config_spec.rb18
-rw-r--r--spec/lib/text_formatter_spec.rb313
-rw-r--r--spec/models/media_attachment_spec.rb8
-rw-r--r--spec/services/activitypub/process_status_update_service_spec.rb31
-rw-r--r--spec/services/after_block_service_spec.rb8
-rw-r--r--spec/services/delete_account_service_spec.rb14
-rw-r--r--spec/services/mute_service_spec.rb22
-rw-r--r--spec/services/notify_service_spec.rb46
-rw-r--r--spec/services/suspend_account_service_spec.rb12
-rw-r--r--spec/services/unsuspend_account_service_spec.rb26
-rw-r--r--spec/services/update_status_service_spec.rb17
-rw-r--r--spec/validators/status_length_validator_spec.rb15
23 files changed, 964 insertions, 753 deletions
diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 0f71d697c..1779fb7c0 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -194,9 +194,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
   end
 
   describe 'POST #unblock_email' do
-    subject do
-      -> { post :unblock_email, params: { id: account.id } }
-    end
+    subject { post :unblock_email, params: { id: account.id } }
 
     let(:current_user) { Fabricate(:user, admin: admin) }
     let(:account) { Fabricate(:account, suspended: true) }
@@ -206,11 +204,11 @@ RSpec.describe Admin::AccountsController, type: :controller do
       let(:admin) { true }
 
       it 'succeeds in removing email blocks' do
-        is_expected.to change { CanonicalEmailBlock.where(reference_account: account).count }.from(1).to(0)
+        expect { subject }.to change { CanonicalEmailBlock.where(reference_account: account).count }.from(1).to(0)
       end
 
       it 'redirects to admin account path' do
-        subject.call
+        subject
         expect(response).to redirect_to admin_account_path(account.id)
       end
     end
@@ -219,7 +217,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
       let(:admin) { false }
 
       it 'fails to remove avatar' do
-        subject.call
+        subject
         expect(response).to have_http_status :forbidden
       end
     end
diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
index 3f61bbc0b..b69595f7e 100644
--- a/spec/controllers/api/v1/admin/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
@@ -30,15 +30,44 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
   end
 
   describe 'GET #index' do
+    let!(:remote_account)       { Fabricate(:account, domain: 'example.org') }
+    let!(:other_remote_account) { Fabricate(:account, domain: 'foo.bar') }
+    let!(:suspended_account)    { Fabricate(:account, suspended: true) }
+    let!(:suspended_remote)     { Fabricate(:account, domain: 'foo.bar', suspended: true) }
+    let!(:disabled_account)     { Fabricate(:user, disabled: true).account }
+    let!(:pending_account)      { Fabricate(:user, approved: false).account }
+    let!(:admin_account)        { user.account }
+
+    let(:params) { {} }
+
     before do
-      get :index
+      pending_account.user.update(approved: false)
+      get :index, params: params
     end
 
     it_behaves_like 'forbidden for wrong scope', 'write:statuses'
     it_behaves_like 'forbidden for wrong role', 'user'
 
-    it 'returns http success' do
-      expect(response).to have_http_status(200)
+    [
+      [{ active: 'true', local: 'true', staff: 'true' }, [:admin_account]],
+      [{ by_domain: 'example.org', remote: 'true' }, [:remote_account]],
+      [{ suspended: 'true' }, [:suspended_account]],
+      [{ disabled: 'true' }, [:disabled_account]],
+      [{ pending: 'true' }, [:pending_account]],
+    ].each do |params, expected_results|
+      context "when called with #{params.inspect}" do
+        let(:params) { params }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it "returns the correct accounts (#{expected_results.inspect})" do
+          json = body_as_json
+
+          expect(json.map { |a| a[:id].to_i }).to eq (expected_results.map { |symbol| send(symbol).id })
+        end
+      end
     end
   end
 
diff --git a/spec/controllers/api/v2/admin/accounts_controller_spec.rb b/spec/controllers/api/v2/admin/accounts_controller_spec.rb
new file mode 100644
index 000000000..3212ddb84
--- /dev/null
+++ b/spec/controllers/api/v2/admin/accounts_controller_spec.rb
@@ -0,0 +1,73 @@
+require 'rails_helper'
+
+RSpec.describe Api::V2::Admin::AccountsController, type: :controller do
+  render_views
+
+  let(:role)   { 'moderator' }
+  let(:user)   { Fabricate(:user, role: role) }
+  let(:scopes) { 'admin:read admin:write' }
+  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  shared_examples 'forbidden for wrong scope' do |wrong_scope|
+    let(:scopes) { wrong_scope }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  shared_examples 'forbidden for wrong role' do |wrong_role|
+    let(:role) { wrong_role }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  describe 'GET #index' do
+    let!(:remote_account)       { Fabricate(:account, domain: 'example.org') }
+    let!(:other_remote_account) { Fabricate(:account, domain: 'foo.bar') }
+    let!(:suspended_account)    { Fabricate(:account, suspended: true) }
+    let!(:suspended_remote)     { Fabricate(:account, domain: 'foo.bar', suspended: true) }
+    let!(:disabled_account)     { Fabricate(:user, disabled: true).account }
+    let!(:pending_account)      { Fabricate(:user, approved: false).account }
+    let!(:admin_account)        { user.account }
+
+    let(:params) { {} }
+
+    before do
+      pending_account.user.update(approved: false)
+      get :index, params: params
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', 'user'
+
+    [
+      [{ status: 'active', origin: 'local', permissions: 'staff' }, [:admin_account]],
+      [{ by_domain: 'example.org', origin: 'remote' }, [:remote_account]],
+      [{ status: 'suspended' }, [:suspended_remote, :suspended_account]],
+      [{ status: 'disabled' }, [:disabled_account]],
+      [{ status: 'pending' }, [:pending_account]],
+    ].each do |params, expected_results|
+      context "when called with #{params.inspect}" do
+        let(:params) { params }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it "returns the correct accounts (#{expected_results.inspect})" do
+          json = body_as_json
+
+          expect(json.map { |a| a[:id].to_i }).to eq (expected_results.map { |symbol| send(symbol).id })
+        end
+      end
+    end
+  end
+end
diff --git a/spec/controllers/settings/exports/bookmarks_controller_specs.rb b/spec/controllers/settings/exports/bookmarks_controller_spec.rb
index 85761577b..a06c02e0c 100644
--- a/spec/controllers/settings/exports/bookmarks_controller_specs.rb
+++ b/spec/controllers/settings/exports/bookmarks_controller_spec.rb
@@ -3,11 +3,16 @@ require 'rails_helper'
 describe Settings::Exports::BookmarksController do
   render_views
 
+  let(:user)    { Fabricate(:user) }
+  let(:account) { Fabricate(:account, domain: 'foo.bar') }
+  let(:status)  { Fabricate(:status, account: account, uri: 'https://foo.bar/statuses/1312') }
+
   describe 'GET #index' do
-    it 'returns a csv of the bookmarked toots' do
-      user = Fabricate(:user)
-      user.account.bookmarks.create!(status: Fabricate(:status, uri: 'https://foo.bar/statuses/1312'))
+    before do
+      user.account.bookmarks.create!(status: status)
+    end
 
+    it 'returns a csv of the bookmarked toots' do
       sign_in user, scope: :user
       get :index, format: :csv
 
diff --git a/spec/lib/advanced_text_formatter_spec.rb b/spec/lib/advanced_text_formatter_spec.rb
new file mode 100644
index 000000000..4e859c93c
--- /dev/null
+++ b/spec/lib/advanced_text_formatter_spec.rb
@@ -0,0 +1,282 @@
+require 'rails_helper'
+
+RSpec.describe AdvancedTextFormatter do
+  describe '#to_s' do
+    let(:preloaded_accounts) { nil }
+    let(:content_type) { 'text/markdown' }
+
+    subject { described_class.new(text, preloaded_accounts: preloaded_accounts, content_type: content_type).to_s }
+
+    context 'given a markdown source' do
+      let(:content_type) { 'text/markdown' }
+
+      context 'given text containing plain text' do
+        let(:text) { 'text' }
+
+        it 'paragraphizes the text' do
+          is_expected.to eq '<p>text</p>'
+        end
+      end
+
+      context 'given text containing line feeds' do
+        let(:text) { "line\nfeed" }
+
+        it 'removes line feeds' do
+          is_expected.not_to include "\n"
+        end
+      end
+
+      context 'given some inline code using backticks' do
+        let(:text) { 'test `foo` bar' }
+
+        it 'formats code using <code>' do
+          is_expected.to include 'test <code>foo</code> bar'
+        end
+      end
+
+      context 'given a block code' do
+        let(:text) { "test\n\n```\nint main(void) {\n  return 0;\n}\n```\n" }
+
+        it 'formats code using <pre> and <code>' do
+          is_expected.to include '<pre><code>int main'
+        end
+      end
+
+      context 'given some quote' do
+        let(:text) { "> foo\n\nbar" }
+
+        it 'formats code using <code>' do
+          is_expected.to include '<blockquote><p>foo</p></blockquote>'
+        end
+      end
+
+      context 'given text containing linkable mentions' do
+        let(:preloaded_accounts) { [Fabricate(:account, username: 'alice')] }
+        let(:text) { '@alice' }
+
+        it 'creates a mention link' do
+          is_expected.to include '<a href="https://cb6e6126.ngrok.io/@alice" class="u-url mention">@<span>alice</span></a></span>'
+        end
+      end
+
+      context 'given text containing unlinkable mentions' do
+        let(:preloaded_accounts) { [] }
+        let(:text) { '@alice' }
+
+        it 'does not create a mention link' do
+          is_expected.to include '@alice'
+        end
+      end
+
+      context 'given a stand-alone medium URL' do
+        let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4"'
+        end
+      end
+
+      context 'given a stand-alone google URL' do
+        let(:text) { 'http://google.com' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="http://google.com"'
+        end
+      end
+
+      context 'given a stand-alone URL with a newer TLD' do
+        let(:text) { 'http://example.gay' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="http://example.gay"'
+        end
+      end
+
+      context 'given a stand-alone IDN URL' do
+        let(:text) { 'https://nic.みんな/' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://nic.みんな/"'
+        end
+
+        it 'has display URL' do
+          is_expected.to include '<span class="">nic.みんな/</span>'
+        end
+      end
+
+      context 'given a URL with a trailing period' do
+        let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' }
+
+        it 'matches the full URL but not the period' do
+          is_expected.to include 'href="http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona"'
+        end
+      end
+
+      context 'given a URL enclosed with parentheses' do
+        let(:text) { '(http://google.com/)' }
+
+        it 'matches the full URL but not the parentheses' do
+          is_expected.to include 'href="http://google.com/"'
+        end
+      end
+
+      context 'given a URL with a trailing exclamation point' do
+        let(:text) { 'http://www.google.com!' }
+
+        it 'matches the full URL but not the exclamation point' do
+          is_expected.to include 'href="http://www.google.com"'
+        end
+      end
+
+      context 'given a URL with a trailing single quote' do
+        let(:text) { "http://www.google.com'" }
+
+        it 'matches the full URL but not the single quote' do
+          is_expected.to include 'href="http://www.google.com"'
+        end
+      end
+    end
+
+    context 'given a URL with a trailing angle bracket' do
+      let(:text) { 'http://www.google.com>' }
+
+      it 'matches the full URL but not the angle bracket' do
+        is_expected.to include 'href="http://www.google.com"'
+      end
+    end
+
+    context 'given a URL with a query string' do
+      context 'with escaped unicode character' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;q=autolink"'
+        end
+      end
+
+      context 'with unicode character' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&amp;q=autolink"'
+        end
+      end
+
+      context 'with unicode character at the end' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"'
+        end
+      end
+
+      context 'with escaped and not escaped unicode characters' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' }
+
+        it 'preserves escaped unicode characters' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;utf81=✓&amp;q=autolink"'
+        end
+      end
+
+      context 'given a URL with parentheses in it' do
+        let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://en.wikipedia.org/wiki/Diaspora_(software)"'
+        end
+      end
+
+      context 'given a URL in quotation marks' do
+        let(:text) { '"https://example.com/"' }
+
+        it 'does not match the quotation marks' do
+          is_expected.to include 'href="https://example.com/"'
+        end
+      end
+
+      context 'given a URL in angle brackets' do
+        let(:text) { '<https://example.com/>' }
+
+        it 'does not match the angle brackets' do
+          is_expected.to include 'href="https://example.com/"'
+        end
+      end
+
+      context 'given a URL containing unsafe code (XSS attack, invisible part)' do
+        let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
+
+        it 'does not include the HTML in the URL' do
+          is_expected.to include '"http://example.com/blahblahblahblah/a"'
+        end
+
+        it 'does not include a script tag' do
+          is_expected.to_not include '<script>'
+        end
+      end
+
+      context 'given text containing HTML code (script tag)' do
+        let(:text) { '<script>alert("Hello")</script>' }
+
+        it 'does not include a script tag' do
+          is_expected.to_not include '<script>'
+        end
+      end
+
+      context 'given text containing HTML (XSS attack)' do
+        let(:text) { %q{<img src="javascript:alert('XSS');">} }
+
+        it 'does not include the javascript' do
+          is_expected.to_not include 'href="javascript:'
+        end
+      end
+
+      context 'given an invalid URL' do
+        let(:text) { 'http://www\.google\.com' }
+
+        it 'outputs the raw URL' do
+          is_expected.to eq '<p>http://www\.google\.com</p>'
+        end
+      end
+
+      context 'given text containing a hashtag' do
+        let(:text)  { '#hashtag' }
+
+        it 'creates a hashtag link' do
+          is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
+        end
+      end
+
+      context 'given text containing a hashtag with Unicode chars' do
+        let(:text)  { '#hashtagタグ' }
+
+        it 'creates a hashtag link' do
+          is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagタグ</span></a>'
+        end
+      end
+
+      context 'given text with a stand-alone xmpp: URI' do
+        let(:text) { 'xmpp:user@instance.com' }
+
+        it 'matches the full URI' do
+          is_expected.to include 'href="xmpp:user@instance.com"'
+        end
+      end
+
+      context 'given text with an xmpp: URI with a query-string' do
+        let(:text) { 'please join xmpp:muc@instance.com?join right now' }
+
+        it 'matches the full URI' do
+          is_expected.to include 'href="xmpp:muc@instance.com?join"'
+        end
+      end
+
+      context 'given text containing a magnet: URI' do
+        let(:text) { 'wikipedia gives this example of a magnet uri: magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a' }
+
+        it 'matches the full URI' do
+          is_expected.to include 'href="magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a"'
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/emoji_formatter_spec.rb b/spec/lib/emoji_formatter_spec.rb
new file mode 100644
index 000000000..129445aa5
--- /dev/null
+++ b/spec/lib/emoji_formatter_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+RSpec.describe EmojiFormatter do
+  let!(:emoji) { Fabricate(:custom_emoji, shortcode: 'coolcat') }
+
+  def preformat_text(str)
+    TextFormatter.new(str).to_s
+  end
+
+  describe '#to_s' do
+    subject { described_class.new(text, emojis).to_s }
+
+    let(:emojis) { [emoji] }
+
+    context 'given text that is not marked as html-safe' do
+      let(:text) { 'Foo' }
+
+      it 'raises an argument error' do
+        expect { subject }.to raise_error ArgumentError
+      end
+    end
+
+    context 'given text with an emoji shortcode at the start' do
+      let(:text) { preformat_text(':coolcat: Beep boop') }
+
+      it 'converts the shortcode to an image tag' do
+        is_expected.to match(/<img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
+      end
+    end
+
+    context 'given text with an emoji shortcode in the middle' do
+      let(:text) { preformat_text('Beep :coolcat: boop') }
+
+      it 'converts the shortcode to an image tag' do
+        is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
+      end
+    end
+
+    context 'given text with concatenated emoji shortcodes' do
+      let(:text) { preformat_text(':coolcat::coolcat:') }
+
+      it 'does not touch the shortcodes' do
+        is_expected.to match(/:coolcat::coolcat:/)
+      end
+    end
+
+    context 'given text with an emoji shortcode at the end' do
+      let(:text) { preformat_text('Beep boop :coolcat:') }
+
+      it 'converts the shortcode to an image tag' do
+        is_expected.to match(/boop <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
+      end
+    end
+  end
+end
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
deleted file mode 100644
index 73cb39550..000000000
--- a/spec/lib/formatter_spec.rb
+++ /dev/null
@@ -1,638 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe Formatter do
-  let(:local_account)  { Fabricate(:account, domain: nil, username: 'alice') }
-  let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') }
-
-  shared_examples 'encode and link URLs' do
-    context 'given a stand-alone medium URL' do
-      let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4"'
-      end
-    end
-
-    context 'given a stand-alone google URL' do
-      let(:text) { 'http://google.com' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="http://google.com"'
-      end
-    end
-
-    context 'given a stand-alone URL with a newer TLD' do
-      let(:text) { 'http://example.gay' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="http://example.gay"'
-      end
-    end
-
-    context 'given a stand-alone IDN URL' do
-      let(:text) { 'https://nic.みんな/' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://nic.みんな/"'
-      end
-
-      it 'has display URL' do
-        is_expected.to include '<span class="">nic.みんな/</span>'
-      end
-    end
-
-    context 'given a URL with a trailing period' do
-      let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' }
-
-      it 'matches the full URL but not the period' do
-        is_expected.to include 'href="http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona"'
-      end
-    end
-
-    context 'given a URL enclosed with parentheses' do
-      let(:text) { '(http://google.com/)' }
-
-      it 'matches the full URL but not the parentheses' do
-        is_expected.to include 'href="http://google.com/"'
-      end
-    end
-
-    context 'given a URL with a trailing exclamation point' do
-      let(:text) { 'http://www.google.com!' }
-
-      it 'matches the full URL but not the exclamation point' do
-        is_expected.to include 'href="http://www.google.com"'
-      end
-    end
-
-    context 'given a URL with a trailing single quote' do
-      let(:text) { "http://www.google.com'" }
-
-      it 'matches the full URL but not the single quote' do
-        is_expected.to include 'href="http://www.google.com"'
-      end
-    end
-
-    context 'given a URL with a trailing angle bracket' do
-      let(:text) { 'http://www.google.com>' }
-
-      it 'matches the full URL but not the angle bracket' do
-        is_expected.to include 'href="http://www.google.com"'
-      end
-    end
-
-    context 'given a URL with a query string' do
-      context 'with escaped unicode character' do
-        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
-
-        it 'matches the full URL' do
-          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;q=autolink"'
-        end
-      end
-
-      context 'with unicode character' do
-        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' }
-
-        it 'matches the full URL' do
-          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&amp;q=autolink"'
-        end
-      end
-
-      context 'with unicode character at the end' do
-        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' }
-
-        it 'matches the full URL' do
-          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"'
-        end
-      end
-
-      context 'with escaped and not escaped unicode characters' do
-        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' }
-
-        it 'preserves escaped unicode characters' do
-          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;utf81=✓&amp;q=autolink"'
-        end
-      end
-    end
-
-    context 'given a URL with parentheses in it' do
-      let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://en.wikipedia.org/wiki/Diaspora_(software)"'
-      end
-    end
-
-    context 'given a URL in quotation marks' do
-      let(:text) { '"https://example.com/"' }
-
-      it 'does not match the quotation marks' do
-        is_expected.to include 'href="https://example.com/"'
-      end
-    end
-
-    context 'given a URL in angle brackets' do
-      let(:text) { '<https://example.com/>' }
-
-      it 'does not match the angle brackets' do
-        is_expected.to include 'href="https://example.com/"'
-      end
-    end
-
-    context 'given a URL with Japanese path string' do
-      let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://ja.wikipedia.org/wiki/日本"'
-      end
-    end
-
-    context 'given a URL with Korean path string' do
-      let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://ko.wikipedia.org/wiki/대한민국"'
-      end
-    end
-
-    context 'given a URL with a full-width space' do
-      let(:text) { 'https://example.com/ abc123' }
-
-      it 'does not match the full-width space' do
-        is_expected.to include 'href="https://example.com/"'
-      end
-    end
-
-    context 'given a URL in Japanese quotation marks' do
-      let(:text) { '「[https://example.org/」' }
-
-      it 'does not match the quotation marks' do
-        is_expected.to include 'href="https://example.org/"'
-      end
-    end
-
-    context 'given a URL with Simplified Chinese path string' do
-      let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://baike.baidu.com/item/中华人民共和国"'
-      end
-    end
-
-    context 'given a URL with Traditional Chinese path string' do
-      let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' }
-
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://zh.wikipedia.org/wiki/臺灣"'
-      end
-    end
-
-    context 'given a URL containing unsafe code (XSS attack, visible part)' do
-      let(:text) { %q{http://example.com/b<del>b</del>} }
-
-      it 'does not include the HTML in the URL' do
-        is_expected.to include '"http://example.com/b"'
-      end
-
-      it 'escapes the HTML' do
-        is_expected.to include '&lt;del&gt;b&lt;/del&gt;'
-      end
-    end
-
-    context 'given a URL containing unsafe code (XSS attack, invisible part)' do
-      let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
-
-      it 'does not include the HTML in the URL' do
-        is_expected.to include '"http://example.com/blahblahblahblah/a"'
-      end
-
-      it 'escapes the HTML' do
-        is_expected.to include '&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;'
-      end
-    end
-
-    context 'given text containing HTML code (script tag)' do
-      let(:text) { '<script>alert("Hello")</script>' }
-
-      it 'escapes the HTML' do
-        is_expected.to include '<p>&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;</p>'
-      end
-    end
-
-    context 'given text containing HTML (XSS attack)' do
-      let(:text) { %q{<img src="javascript:alert('XSS');">} }
-
-      it 'escapes the HTML' do
-        is_expected.to include '<p>&lt;img src=&quot;javascript:alert(&apos;XSS&apos;);&quot;&gt;</p>'
-      end
-    end
-
-    context 'given an invalid URL' do
-      let(:text) { 'http://www\.google\.com' }
-
-      it 'outputs the raw URL' do
-        is_expected.to eq '<p>http://www\.google\.com</p>'
-      end
-    end
-
-    context 'given text containing a hashtag' do
-      let(:text)  { '#hashtag' }
-
-      it 'creates a hashtag link' do
-        is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
-      end
-    end
-
-    context 'given text containing a hashtag with Unicode chars' do
-      let(:text)  { '#hashtagタグ' }
-
-      it 'creates a hashtag link' do
-        is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagタグ</span></a>'
-      end
-    end
-
-    context 'given a stand-alone xmpp: URI' do
-      let(:text) { 'xmpp:user@instance.com' }
-
-      it 'matches the full URI' do
-        is_expected.to include 'href="xmpp:user@instance.com"'
-      end
-    end
-
-    context 'given a an xmpp: URI with a query-string' do
-      let(:text) { 'please join xmpp:muc@instance.com?join right now' }
-
-      it 'matches the full URI' do
-        is_expected.to include 'href="xmpp:muc@instance.com?join"'
-      end
-    end
-
-    context 'given text containing a magnet: URI' do
-      let(:text) { 'wikipedia gives this example of a magnet uri: magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a' }
-
-      it 'matches the full URI' do
-        is_expected.to include 'href="magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a"'
-      end
-    end
-  end
-
-  describe '#format_spoiler' do
-    subject { Formatter.instance.format_spoiler(status) }
-
-    context 'given a post containing plain text' do
-      let(:status) { Fabricate(:status, text: 'text', spoiler_text: 'Secret!', uri: nil) }
-
-      it 'Returns the spoiler text' do
-        is_expected.to eq 'Secret!'
-      end
-    end
-
-    context 'given a post with an emoji shortcode at the start' do
-      let!(:emoji) { Fabricate(:custom_emoji) }
-      let(:status) { Fabricate(:status, text: 'text', spoiler_text: ':coolcat: Secret!', uri: nil) }
-      let(:text) { ':coolcat: Beep boop' }
-
-      it 'converts the shortcode to an image tag' do
-        is_expected.to match(/<img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-      end
-    end
-  end
-
-  describe '#format' do
-    subject { Formatter.instance.format(status) }
-
-    context 'given a post with local status' do
-      context 'given a reblogged post' do
-        let(:reblog) { Fabricate(:status, account: local_account, text: 'Hello world', uri: nil) }
-        let(:status) { Fabricate(:status, reblog: reblog) }
-
-        it 'returns original status with credit to its author' do
-          is_expected.to include 'RT <span class="h-card"><a href="https://cb6e6126.ngrok.io/@alice" class="u-url mention">@<span>alice</span></a></span> Hello world'
-        end
-      end
-
-      context 'given a post containing plain text' do
-        let(:status) { Fabricate(:status, text: 'text', uri: nil) }
-
-        it 'paragraphizes the text' do
-          is_expected.to eq '<p>text</p>'
-        end
-      end
-
-      context 'given a post containing line feeds' do
-        let(:status) { Fabricate(:status, text: "line\nfeed", uri: nil) }
-
-        it 'removes line feeds' do
-          is_expected.not_to include "\n"
-        end
-      end
-
-      context 'given a post containing linkable mentions' do
-        let(:status) { Fabricate(:status, mentions: [ Fabricate(:mention, account: local_account) ], text: '@alice') }
-
-        it 'creates a mention link' do
-          is_expected.to include '<a href="https://cb6e6126.ngrok.io/@alice" class="u-url mention">@<span>alice</span></a></span>'
-        end
-      end
-
-      context 'given a post containing unlinkable mentions' do
-        let(:status) { Fabricate(:status, text: '@alice', uri: nil) }
-
-        it 'does not create a mention link' do
-          is_expected.to include '@alice'
-        end
-      end
-
-      context do
-        let(:content_type) { 'text/plain' }
-
-        subject do
-          status = Fabricate(:status, text: text, content_type: content_type, uri: nil)
-          Formatter.instance.format(status)
-        end
-
-        context 'given an invalid URL (invalid port)' do
-          let(:text) { 'https://foo.bar:X/' }
-          let(:content_type) { 'text/markdown' }
-
-          it 'outputs the raw URL' do
-            is_expected.to eq '<p>https://foo.bar:X/</p>'
-          end
-        end
-
-        include_examples 'encode and link URLs'
-      end
-
-      context 'given a post with custom_emojify option' do
-        let!(:emoji) { Fabricate(:custom_emoji) }
-        let(:status) { Fabricate(:status, account: local_account, text: text) }
-
-        subject { Formatter.instance.format(status, custom_emojify: true) }
-
-        context 'given a post with an emoji shortcode at the start' do
-          let(:text) { ':coolcat: Beep boop' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/<p><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-
-        context 'given a post with an emoji shortcode in the middle' do
-          let(:text) { 'Beep :coolcat: boop' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-
-        context 'given a post with concatenated emoji shortcodes' do
-          let(:text) { ':coolcat::coolcat:' }
-
-          it 'does not touch the shortcodes' do
-            is_expected.to match(/:coolcat::coolcat:/)
-          end
-        end
-
-        context 'given a post with an emoji shortcode at the end' do
-          let(:text) { 'Beep boop :coolcat:' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/boop <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-      end
-    end
-
-    context 'given a post with remote status' do
-      let(:status) { Fabricate(:status, account: remote_account, text: 'Beep boop') }
-
-      it 'reformats the post' do
-        is_expected.to eq 'Beep boop'
-      end
-
-      context 'given a post with custom_emojify option' do
-        let!(:emoji) { Fabricate(:custom_emoji, domain: remote_account.domain) }
-        let(:status) { Fabricate(:status, account: remote_account, text: text) }
-
-        subject { Formatter.instance.format(status, custom_emojify: true) }
-
-        context 'given a post with an emoji shortcode at the start' do
-          let(:text) { '<p>:coolcat: Beep boop<br />' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/<p><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-
-        context 'given a post with an emoji shortcode in the middle' do
-          let(:text) { '<p>Beep :coolcat: boop</p>' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-
-        context 'given a post with concatenated emoji' do
-          let(:text) { '<p>:coolcat::coolcat:</p>' }
-
-          it 'does not touch the shortcodes' do
-            is_expected.to match(/<p>:coolcat::coolcat:<\/p>/)
-          end
-        end
-
-        context 'given a post with an emoji shortcode at the end' do
-          let(:text) { '<p>Beep boop<br />:coolcat:</p>' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/<br><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-      end
-    end
-  end
-
-  describe '#reformat' do
-    subject { Formatter.instance.reformat(text) }
-
-    context 'given a post containing plain text' do
-      let(:text) { 'Beep boop' }
-
-      it 'keeps the plain text' do
-        is_expected.to include 'Beep boop'
-      end
-    end
-
-    context 'given a post containing script tags' do
-      let(:text) { '<script>alert("Hello")</script>' }
-
-      it 'strips the scripts' do
-        is_expected.to_not include '<script>alert("Hello")</script>'
-      end
-    end
-
-    context 'given a post containing malicious classes' do
-      let(:text) { '<span class="mention	status__content__spoiler-link">Show more</span>' }
-
-      it 'strips the malicious classes' do
-        is_expected.to_not include 'status__content__spoiler-link'
-      end
-    end
-  end
-
-  describe '#plaintext' do
-    subject { Formatter.instance.plaintext(status) }
-
-    context 'given a post with local status' do
-      let(:status) { Fabricate(:status, text: '<p>a text by a nerd who uses an HTML tag in text</p>', content_type: content_type, uri: nil) }
-      let(:content_type) { 'text/plain' }
-
-      it 'returns the raw text' do
-        is_expected.to eq '<p>a text by a nerd who uses an HTML tag in text</p>'
-      end
-    end
-
-    context 'given a post with remote status' do
-      let(:status) { Fabricate(:status, account: remote_account, text: '<script>alert("Hello")</script>') }
-
-      it 'returns tag-stripped text' do
-        is_expected.to eq ''
-      end
-    end
-  end
-
-  describe '#simplified_format' do
-    subject { Formatter.instance.simplified_format(account) }
-
-    context 'given a post with local status' do
-      let(:account) { Fabricate(:account, domain: nil, note: text) }
-
-      context 'given a post containing linkable mentions for local accounts' do
-        let(:text) { '@alice' }
-
-        before { local_account }
-
-        it 'creates a mention link' do
-          is_expected.to eq '<p><span class="h-card"><a href="https://cb6e6126.ngrok.io/@alice" class="u-url mention">@<span>alice</span></a></span></p>'
-        end
-      end
-
-      context 'given a post containing linkable mentions for remote accounts' do
-        let(:text) { '@bob@remote.test' }
-
-        before { remote_account }
-
-        it 'creates a mention link' do
-          is_expected.to eq '<p><span class="h-card"><a href="https://remote.test/" class="u-url mention">@<span>bob</span></a></span></p>'
-        end
-      end
-
-      context 'given a post containing unlinkable mentions' do
-        let(:text) { '@alice' }
-
-        it 'does not create a mention link' do
-          is_expected.to eq '<p>@alice</p>'
-        end
-      end
-
-      context 'given a post with custom_emojify option' do
-        let!(:emoji) { Fabricate(:custom_emoji) }
-
-        before { account.note = text }
-        subject { Formatter.instance.simplified_format(account, custom_emojify: true) }
-
-        context 'given a post with an emoji shortcode at the start' do
-          let(:text) { ':coolcat: Beep boop' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/<p><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-
-        context 'given a post with an emoji shortcode in the middle' do
-          let(:text) { 'Beep :coolcat: boop' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-
-        context 'given a post with concatenated emoji shortcodes' do
-          let(:text) { ':coolcat::coolcat:' }
-
-          it 'does not touch the shortcodes' do
-            is_expected.to match(/:coolcat::coolcat:/)
-          end
-        end
-
-        context 'given a post with an emoji shortcode at the end' do
-          let(:text) { 'Beep boop :coolcat:' }
-
-          it 'converts the shortcode to an image tag' do
-            is_expected.to match(/boop <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-      end
-
-      include_examples 'encode and link URLs'
-    end
-
-    context 'given a post with remote status' do
-      let(:text) { '<script>alert("Hello")</script>' }
-      let(:account) { Fabricate(:account, domain: 'remote', note: text) }
-
-      it 'reformats' do
-        is_expected.to_not include '<script>alert("Hello")</script>'
-      end
-
-      context 'with custom_emojify option' do
-        let!(:emoji) { Fabricate(:custom_emoji, domain: remote_account.domain) }
-
-        before { remote_account.note = text }
-
-        subject { Formatter.instance.simplified_format(remote_account, custom_emojify: true) }
-
-        context 'given a post with an emoji shortcode at the start' do
-          let(:text) { '<p>:coolcat: Beep boop<br />' }
-
-          it 'converts shortcode to image tag' do
-            is_expected.to match(/<p><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-
-        context 'given a post with an emoji shortcode in the middle' do
-          let(:text) { '<p>Beep :coolcat: boop</p>' }
-
-          it 'converts shortcode to image tag' do
-            is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-
-        context 'given a post with concatenated emoji shortcodes' do
-          let(:text) { '<p>:coolcat::coolcat:</p>' }
-
-          it 'does not touch the shortcodes' do
-            is_expected.to match(/<p>:coolcat::coolcat:<\/p>/)
-          end
-        end
-
-        context 'given a post with an emoji shortcode at the end' do
-          let(:text) { '<p>Beep boop<br />:coolcat:</p>' }
-
-          it 'converts shortcode to image tag' do
-            is_expected.to match(/<br><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
-          end
-        end
-      end
-    end
-  end
-
-  describe '#sanitize' do
-    let(:html) { '<script>alert("Hello")</script>' }
-
-    subject { Formatter.instance.sanitize(html, Sanitize::Config::MASTODON_STRICT) }
-
-    it 'sanitizes' do
-      is_expected.to eq ''
-    end
-  end
-end
diff --git a/spec/lib/html_aware_formatter_spec.rb b/spec/lib/html_aware_formatter_spec.rb
new file mode 100644
index 000000000..18d23abf5
--- /dev/null
+++ b/spec/lib/html_aware_formatter_spec.rb
@@ -0,0 +1,44 @@
+require 'rails_helper'
+
+RSpec.describe HtmlAwareFormatter do
+  describe '#to_s' do
+    subject { described_class.new(text, local).to_s }
+
+    context 'when local' do
+      let(:local) { true }
+      let(:text) { 'Foo bar' }
+
+      it 'returns formatted text' do
+        is_expected.to eq '<p>Foo bar</p>'
+      end
+    end
+
+    context 'when remote' do
+      let(:local) { false }
+
+      context 'given plain text' do
+        let(:text) { 'Beep boop' }
+
+        it 'keeps the plain text' do
+          is_expected.to include 'Beep boop'
+        end
+      end
+
+      context 'given text containing script tags' do
+        let(:text) { '<script>alert("Hello")</script>' }
+
+        it 'strips the scripts' do
+          is_expected.to_not include '<script>alert("Hello")</script>'
+        end
+      end
+
+      context 'given text containing malicious classes' do
+        let(:text) { '<span class="mention  status__content__spoiler-link">Show more</span>' }
+
+        it 'strips the malicious classes' do
+          is_expected.to_not include 'status__content__spoiler-link'
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/link_details_extractor_spec.rb b/spec/lib/link_details_extractor_spec.rb
index 84bb4579c..7ea867c61 100644
--- a/spec/lib/link_details_extractor_spec.rb
+++ b/spec/lib/link_details_extractor_spec.rb
@@ -25,6 +25,14 @@ RSpec.describe LinkDetailsExtractor do
         expect(subject.canonical_url).to eq 'https://foo.com/article'
       end
     end
+
+    context 'when canonical URL is set to "null"' do
+      let(:html) { '<!doctype html><link rel="canonical" href="null" />' }
+
+      it 'ignores the canonical URLs' do
+        expect(subject.canonical_url).to eq original_url
+      end
+    end
   end
 
   context 'when structured data is present' do
diff --git a/spec/lib/plain_text_formatter_spec.rb b/spec/lib/plain_text_formatter_spec.rb
new file mode 100644
index 000000000..c3d0ee630
--- /dev/null
+++ b/spec/lib/plain_text_formatter_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+RSpec.describe PlainTextFormatter do
+  describe '#to_s' do
+    subject { described_class.new(status.text, status.local?).to_s }
+
+    context 'given a post with local status' do
+      let(:status) { Fabricate(:status, text: '<p>a text by a nerd who uses an HTML tag in text</p>', uri: nil) }
+
+      it 'returns the raw text' do
+        is_expected.to eq '<p>a text by a nerd who uses an HTML tag in text</p>'
+      end
+    end
+
+    context 'given a post with remote status' do
+      let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') }
+      let(:status) { Fabricate(:status, account: remote_account, text: '<p>Hello</p><script>alert("Hello")</script>') }
+
+      it 'returns tag-stripped text' do
+        is_expected.to eq 'Hello'
+      end
+    end
+  end
+end
diff --git a/spec/lib/rss/serializer_spec.rb b/spec/lib/rss/serializer_spec.rb
index 0364d13de..1da45d302 100644
--- a/spec/lib/rss/serializer_spec.rb
+++ b/spec/lib/rss/serializer_spec.rb
@@ -13,13 +13,6 @@ describe RSS::Serializer do
 
     subject { RSS::Serializer.new.send(:status_title, status) }
 
-    context 'if destroyed?' do
-      it 'returns "#{account.acct} deleted status"' do
-        status.destroy!
-        expect(subject).to eq "#{account.acct} deleted status"
-      end
-    end
-
     context 'on a toot with long text' do
       let(:text) { "This toot's text is longer than the allowed number of characters" }
 
diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb
index 8bcffb2e5..dc6418e5b 100644
--- a/spec/lib/sanitize_config_spec.rb
+++ b/spec/lib/sanitize_config_spec.rb
@@ -41,18 +41,8 @@ describe Sanitize::Config do
     end
   end
 
-  describe '::MASTODON_STRICT' do
-    subject { Sanitize::Config::MASTODON_STRICT }
-
-    it_behaves_like 'common HTML sanitization'
-
-    it 'keeps a with href and rel tag' do
-      expect(Sanitize.fragment('<a href="http://example.com" rel="tag">Test</a>', subject)).to eq '<a href="http://example.com" rel="tag nofollow noopener noreferrer" target="_blank">Test</a>'
-    end
-  end
-
-  describe '::MASTODON_STRICT with outgoing toots' do
-    subject { Sanitize::Config::MASTODON_STRICT.merge(outgoing: true) }
+  describe '::MASTODON_OUTGOING' do
+    subject { Sanitize::Config::MASTODON_OUTGOING }
 
     around do |example|
       original_web_domain = Rails.configuration.x.web_domain
@@ -62,9 +52,9 @@ describe Sanitize::Config do
 
     it_behaves_like 'common HTML sanitization'
 
-    it 'keeps a with href and rel tag, not adding to rel if url is local' do
+    it 'keeps a with href and rel tag, not adding to rel or target if url is local' do
       Rails.configuration.x.web_domain = 'domain.test'
-      expect(Sanitize.fragment('<a href="http://domain.test/tags/foo" rel="tag">Test</a>', subject)).to eq '<a href="http://domain.test/tags/foo" rel="tag" target="_blank">Test</a>'
+      expect(Sanitize.fragment('<a href="http://domain.test/tags/foo" rel="tag">Test</a>', subject)).to eq '<a href="http://domain.test/tags/foo" rel="tag">Test</a>'
     end
   end
 end
diff --git a/spec/lib/text_formatter_spec.rb b/spec/lib/text_formatter_spec.rb
new file mode 100644
index 000000000..52a9d2498
--- /dev/null
+++ b/spec/lib/text_formatter_spec.rb
@@ -0,0 +1,313 @@
+require 'rails_helper'
+
+RSpec.describe TextFormatter do
+  describe '#to_s' do
+    let(:preloaded_accounts) { nil }
+
+    subject { described_class.new(text, preloaded_accounts: preloaded_accounts).to_s }
+
+    context 'given text containing plain text' do
+      let(:text) { 'text' }
+
+      it 'paragraphizes the text' do
+        is_expected.to eq '<p>text</p>'
+      end
+    end
+
+    context 'given text containing line feeds' do
+      let(:text) { "line\nfeed" }
+
+      it 'removes line feeds' do
+        is_expected.not_to include "\n"
+      end
+    end
+
+    context 'given text containing linkable mentions' do
+      let(:preloaded_accounts) { [Fabricate(:account, username: 'alice')] }
+      let(:text) { '@alice' }
+
+      it 'creates a mention link' do
+        is_expected.to include '<a href="https://cb6e6126.ngrok.io/@alice" class="u-url mention">@<span>alice</span></a></span>'
+      end
+    end
+
+    context 'given text containing unlinkable mentions' do
+      let(:preloaded_accounts) { [] }
+      let(:text) { '@alice' }
+
+      it 'does not create a mention link' do
+        is_expected.to include '@alice'
+      end
+    end
+
+    context 'given a stand-alone medium URL' do
+      let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4"'
+      end
+    end
+
+    context 'given a stand-alone google URL' do
+      let(:text) { 'http://google.com' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="http://google.com"'
+      end
+    end
+
+    context 'given a stand-alone URL with a newer TLD' do
+      let(:text) { 'http://example.gay' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="http://example.gay"'
+      end
+    end
+
+    context 'given a stand-alone IDN URL' do
+      let(:text) { 'https://nic.みんな/' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="https://nic.みんな/"'
+      end
+
+      it 'has display URL' do
+        is_expected.to include '<span class="">nic.みんな/</span>'
+      end
+    end
+
+    context 'given a URL with a trailing period' do
+      let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' }
+
+      it 'matches the full URL but not the period' do
+        is_expected.to include 'href="http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona"'
+      end
+    end
+
+    context 'given a URL enclosed with parentheses' do
+      let(:text) { '(http://google.com/)' }
+
+      it 'matches the full URL but not the parentheses' do
+        is_expected.to include 'href="http://google.com/"'
+      end
+    end
+
+    context 'given a URL with a trailing exclamation point' do
+      let(:text) { 'http://www.google.com!' }
+
+      it 'matches the full URL but not the exclamation point' do
+        is_expected.to include 'href="http://www.google.com"'
+      end
+    end
+
+    context 'given a URL with a trailing single quote' do
+      let(:text) { "http://www.google.com'" }
+
+      it 'matches the full URL but not the single quote' do
+        is_expected.to include 'href="http://www.google.com"'
+      end
+    end
+
+    context 'given a URL with a trailing angle bracket' do
+      let(:text) { 'http://www.google.com>' }
+
+      it 'matches the full URL but not the angle bracket' do
+        is_expected.to include 'href="http://www.google.com"'
+      end
+    end
+
+    context 'given a URL with a query string' do
+      context 'with escaped unicode character' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;q=autolink"'
+        end
+      end
+
+      context 'with unicode character' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&amp;q=autolink"'
+        end
+      end
+
+      context 'with unicode character at the end' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"'
+        end
+      end
+
+      context 'with escaped and not escaped unicode characters' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' }
+
+        it 'preserves escaped unicode characters' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;utf81=✓&amp;q=autolink"'
+        end
+      end
+    end
+
+    context 'given a URL with parentheses in it' do
+      let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="https://en.wikipedia.org/wiki/Diaspora_(software)"'
+      end
+    end
+
+    context 'given a URL in quotation marks' do
+      let(:text) { '"https://example.com/"' }
+
+      it 'does not match the quotation marks' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
+    context 'given a URL in angle brackets' do
+      let(:text) { '<https://example.com/>' }
+
+      it 'does not match the angle brackets' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
+    context 'given a URL with Japanese path string' do
+      let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="https://ja.wikipedia.org/wiki/日本"'
+      end
+    end
+
+    context 'given a URL with Korean path string' do
+      let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="https://ko.wikipedia.org/wiki/대한민국"'
+      end
+    end
+
+    context 'given a URL with a full-width space' do
+      let(:text) { 'https://example.com/ abc123' }
+
+      it 'does not match the full-width space' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
+    context 'given a URL in Japanese quotation marks' do
+      let(:text) { '「[https://example.org/」' }
+
+      it 'does not match the quotation marks' do
+        is_expected.to include 'href="https://example.org/"'
+      end
+    end
+
+    context 'given a URL with Simplified Chinese path string' do
+      let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="https://baike.baidu.com/item/中华人民共和国"'
+      end
+    end
+
+    context 'given a URL with Traditional Chinese path string' do
+      let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' }
+
+      it 'matches the full URL' do
+        is_expected.to include 'href="https://zh.wikipedia.org/wiki/臺灣"'
+      end
+    end
+
+    context 'given a URL containing unsafe code (XSS attack, visible part)' do
+      let(:text) { %q{http://example.com/b<del>b</del>} }
+
+      it 'does not include the HTML in the URL' do
+        is_expected.to include '"http://example.com/b"'
+      end
+
+      it 'escapes the HTML' do
+        is_expected.to include '&lt;del&gt;b&lt;/del&gt;'
+      end
+    end
+
+    context 'given a URL containing unsafe code (XSS attack, invisible part)' do
+      let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
+
+      it 'does not include the HTML in the URL' do
+        is_expected.to include '"http://example.com/blahblahblahblah/a"'
+      end
+
+      it 'escapes the HTML' do
+        is_expected.to include '&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;'
+      end
+    end
+
+    context 'given text containing HTML code (script tag)' do
+      let(:text) { '<script>alert("Hello")</script>' }
+
+      it 'escapes the HTML' do
+        is_expected.to include '<p>&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;</p>'
+      end
+    end
+
+    context 'given text containing HTML (XSS attack)' do
+      let(:text) { %q{<img src="javascript:alert('XSS');">} }
+
+      it 'escapes the HTML' do
+        is_expected.to include '<p>&lt;img src=&quot;javascript:alert(&#39;XSS&#39;);&quot;&gt;</p>'
+      end
+    end
+
+    context 'given an invalid URL' do
+      let(:text) { 'http://www\.google\.com' }
+
+      it 'outputs the raw URL' do
+        is_expected.to eq '<p>http://www\.google\.com</p>'
+      end
+    end
+
+    context 'given text containing a hashtag' do
+      let(:text)  { '#hashtag' }
+
+      it 'creates a hashtag link' do
+        is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
+      end
+    end
+
+    context 'given text containing a hashtag with Unicode chars' do
+      let(:text)  { '#hashtagタグ' }
+
+      it 'creates a hashtag link' do
+        is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagタグ</span></a>'
+      end
+    end
+
+    context 'given text with a stand-alone xmpp: URI' do
+      let(:text) { 'xmpp:user@instance.com' }
+
+      it 'matches the full URI' do
+        is_expected.to include 'href="xmpp:user@instance.com"'
+      end
+    end
+
+    context 'given text with an xmpp: URI with a query-string' do
+      let(:text) { 'please join xmpp:muc@instance.com?join right now' }
+
+      it 'matches the full URI' do
+        is_expected.to include 'href="xmpp:muc@instance.com?join"'
+      end
+    end
+
+    context 'given text containing a magnet: URI' do
+      let(:text) { 'wikipedia gives this example of a magnet uri: magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a' }
+
+      it 'matches the full URI' do
+        is_expected.to include 'href="magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a"'
+      end
+    end
+  end
+end
diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index 7360b23cf..cbd9a09c5 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -186,14 +186,6 @@ RSpec.describe MediaAttachment, type: :model do
     expect(media.valid?).to be false
   end
 
-  describe 'descriptions for remote attachments' do
-    it 'are cut off at 1500 characters' do
-      media = Fabricate(:media_attachment, description: 'foo' * 1000, remote_url: 'http://example.com/blah.jpg')
-
-      expect(media.description.size).to be <= 1_500
-    end
-  end
-
   describe 'size limit validation' do
     it 'rejects video files that are too large' do
       stub_const 'MediaAttachment::IMAGE_LIMIT', 100.megabytes
diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb
index 788c7c9d9..f87adcae1 100644
--- a/spec/services/activitypub/process_status_update_service_spec.rb
+++ b/spec/services/activitypub/process_status_update_service_spec.rb
@@ -46,6 +46,29 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
       expect(status.reload.spoiler_text).to eq 'Show more'
     end
 
+    context 'with no changes at all' do
+      let(:payload) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          id: 'foo',
+          type: 'Note',
+          content: 'Hello world',
+        }
+      end
+
+      before do
+        subject.call(status, json)
+      end
+
+      it 'does not create any edits' do
+        expect(status.reload.edits).to be_empty
+      end
+
+      it 'does not mark status as edited' do
+        expect(status.edited?).to be false
+      end
+    end
+
     context 'with no changes and originally with no ordered_media_attachment_ids' do
       let(:payload) do
         {
@@ -61,8 +84,12 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
         subject.call(status, json)
       end
 
-      it 'does not record an update' do
-        expect(status.reload.edited?).to be false
+      it 'does not create any edits' do
+        expect(status.reload.edits).to be_empty
+      end
+
+      it 'does not mark status as edited' do
+        expect(status.edited?).to be false
       end
     end
 
diff --git a/spec/services/after_block_service_spec.rb b/spec/services/after_block_service_spec.rb
index fe5b26b2b..c09425d7c 100644
--- a/spec/services/after_block_service_spec.rb
+++ b/spec/services/after_block_service_spec.rb
@@ -1,9 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe AfterBlockService, type: :service do
-  subject do
-    -> { described_class.new.call(account, target_account) }
-  end
+  subject { described_class.new.call(account, target_account) }
 
   let(:account)              { Fabricate(:account) }
   let(:target_account)       { Fabricate(:account) }
@@ -24,7 +22,7 @@ RSpec.describe AfterBlockService, type: :service do
       FeedManager.instance.push_to_home(account, other_account_status)
       FeedManager.instance.push_to_home(account, other_account_reblog)
 
-      is_expected.to change {
+      expect { subject }.to change {
         Redis.current.zrange(home_timeline_key, 0, -1)
       }.from([status.id.to_s, other_account_status.id.to_s, other_account_reblog.id.to_s]).to([other_account_status.id.to_s])
     end
@@ -43,7 +41,7 @@ RSpec.describe AfterBlockService, type: :service do
       FeedManager.instance.push_to_list(list, other_account_status)
       FeedManager.instance.push_to_list(list, other_account_reblog)
 
-      is_expected.to change {
+      expect { subject }.to change {
         Redis.current.zrange(list_timeline_key, 0, -1)
       }.from([status.id.to_s, other_account_status.id.to_s, other_account_reblog.id.to_s]).to([other_account_status.id.to_s])
     end
diff --git a/spec/services/delete_account_service_spec.rb b/spec/services/delete_account_service_spec.rb
index 9c785fc17..1fbe4d07c 100644
--- a/spec/services/delete_account_service_spec.rb
+++ b/spec/services/delete_account_service_spec.rb
@@ -23,12 +23,10 @@ RSpec.describe DeleteAccountService, type: :service do
 
     let!(:account_note) { Fabricate(:account_note, account: account) }
 
-    subject do
-      -> { described_class.new.call(account) }
-    end
+    subject { described_class.new.call(account) }
 
     it 'deletes associated owned records' do
-      is_expected.to change {
+      expect { subject }.to change {
         [
           account.statuses,
           account.media_attachments,
@@ -43,7 +41,7 @@ RSpec.describe DeleteAccountService, type: :service do
     end
 
     it 'deletes associated target records' do
-      is_expected.to change {
+      expect { subject }.to change {
         [
           AccountPin.where(target_account: account),
         ].map(&:count)
@@ -51,7 +49,7 @@ RSpec.describe DeleteAccountService, type: :service do
     end
 
     it 'deletes associated target notifications' do
-      is_expected.to change {
+      expect { subject }.to change {
         [
           'poll', 'favourite', 'status', 'mention', 'follow'
         ].map { |type| Notification.where(type: type).count }
@@ -73,7 +71,7 @@ RSpec.describe DeleteAccountService, type: :service do
       let!(:local_follower) { Fabricate(:account) }
 
       it 'sends a delete actor activity to all known inboxes' do
-        subject.call
+        subject
         expect(a_request(:post, "https://alice.com/inbox")).to have_been_made.once
         expect(a_request(:post, "https://bob.com/inbox")).to have_been_made.once
       end
@@ -91,7 +89,7 @@ RSpec.describe DeleteAccountService, type: :service do
       let!(:local_follower) { Fabricate(:account) }
 
       it 'sends a reject follow to follower inboxes' do
-        subject.call
+        subject
         expect(a_request(:post, account.inbox_url)).to have_been_made.once
       end
     end
diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb
index 4bb839b8d..bdec1c67b 100644
--- a/spec/services/mute_service_spec.rb
+++ b/spec/services/mute_service_spec.rb
@@ -1,9 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe MuteService, type: :service do
-  subject do
-    -> { described_class.new.call(account, target_account) }
-  end
+  subject { described_class.new.call(account, target_account) }
 
   let(:account) { Fabricate(:account) }
   let(:target_account) { Fabricate(:account) }
@@ -21,45 +19,41 @@ RSpec.describe MuteService, type: :service do
       FeedManager.instance.push_to_home(account, status)
       FeedManager.instance.push_to_home(account, other_account_status)
 
-      is_expected.to change {
+      expect { subject }.to change {
         Redis.current.zrange(home_timeline_key, 0, -1)
       }.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s])
     end
   end
 
   it 'mutes account' do
-    is_expected.to change {
+    expect { subject }.to change {
       account.muting?(target_account)
     }.from(false).to(true)
   end
 
   context 'without specifying a notifications parameter' do
     it 'mutes notifications from the account' do
-      is_expected.to change {
+      expect { subject }.to change {
         account.muting_notifications?(target_account)
       }.from(false).to(true)
     end
   end
 
   context 'with a true notifications parameter' do
-    subject do
-      -> { described_class.new.call(account, target_account, notifications: true) }
-    end
+    subject { described_class.new.call(account, target_account, notifications: true) }
 
     it 'mutes notifications from the account' do
-      is_expected.to change {
+      expect { subject }.to change {
         account.muting_notifications?(target_account)
       }.from(false).to(true)
     end
   end
 
   context 'with a false notifications parameter' do
-    subject do
-      -> { described_class.new.call(account, target_account, notifications: false) }
-    end
+    subject { described_class.new.call(account, target_account, notifications: false) }
 
     it 'does not mute notifications from the account' do
-      is_expected.to_not change {
+      expect { subject }.to_not change {
         account.muting_notifications?(target_account)
       }.from(false)
     end
diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb
index 83e62ff36..67dd0483b 100644
--- a/spec/services/notify_service_spec.rb
+++ b/spec/services/notify_service_spec.rb
@@ -1,9 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe NotifyService, type: :service do
-  subject do
-    -> { described_class.new.call(recipient, type, activity) }
-  end
+  subject { described_class.new.call(recipient, type, activity) }
 
   let(:user) { Fabricate(:user) }
   let(:recipient) { user.account }
@@ -11,42 +9,42 @@ RSpec.describe NotifyService, type: :service do
   let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) }
   let(:type) { :follow }
 
-  it { is_expected.to change(Notification, :count).by(1) }
+  it { expect { subject }.to change(Notification, :count).by(1) }
 
   it 'does not notify when sender is blocked' do
     recipient.block!(sender)
-    is_expected.to_not change(Notification, :count)
+    expect { subject }.to_not change(Notification, :count)
   end
 
   it 'does not notify when sender is muted with hide_notifications' do
     recipient.mute!(sender, notifications: true)
-    is_expected.to_not change(Notification, :count)
+    expect { subject }.to_not change(Notification, :count)
   end
 
   it 'does notify when sender is muted without hide_notifications' do
     recipient.mute!(sender, notifications: false)
-    is_expected.to change(Notification, :count)
+    expect { subject }.to change(Notification, :count)
   end
 
   it 'does not notify when sender\'s domain is blocked' do
     recipient.block_domain!(sender.domain)
-    is_expected.to_not change(Notification, :count)
+    expect { subject }.to_not change(Notification, :count)
   end
 
   it 'does still notify when sender\'s domain is blocked but sender is followed' do
     recipient.block_domain!(sender.domain)
     recipient.follow!(sender)
-    is_expected.to change(Notification, :count)
+    expect { subject }.to change(Notification, :count)
   end
 
   it 'does not notify when sender is silenced and not followed' do
     sender.silence!
-    is_expected.to_not change(Notification, :count)
+    expect { subject }.to_not change(Notification, :count)
   end
 
   it 'does not notify when recipient is suspended' do
     recipient.suspend!
-    is_expected.to_not change(Notification, :count)
+    expect { subject }.to_not change(Notification, :count)
   end
   
   context 'for direct messages' do
@@ -61,7 +59,7 @@ RSpec.describe NotifyService, type: :service do
       let(:enabled) { true }
 
       it 'does not notify' do
-        is_expected.to_not change(Notification, :count)
+        expect { subject }.to_not change(Notification, :count)
       end
 
       context 'if the message chain is initiated by recipient, but is not direct message' do
@@ -70,7 +68,7 @@ RSpec.describe NotifyService, type: :service do
         let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
 
         it 'does not notify' do
-          is_expected.to_not change(Notification, :count)
+          expect { subject }.to_not change(Notification, :count)
         end
       end
 
@@ -81,7 +79,7 @@ RSpec.describe NotifyService, type: :service do
         let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: dummy_reply)) }
 
         it 'does not notify' do
-          is_expected.to_not change(Notification, :count)
+          expect { subject }.to_not change(Notification, :count)
         end
       end
 
@@ -91,7 +89,7 @@ RSpec.describe NotifyService, type: :service do
         let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
 
         it 'does notify' do
-          is_expected.to change(Notification, :count)
+          expect { subject }.to change(Notification, :count)
         end
       end
     end
@@ -100,7 +98,7 @@ RSpec.describe NotifyService, type: :service do
       let(:enabled) { false }
 
       it 'does notify' do
-        is_expected.to change(Notification, :count)
+        expect { subject }.to change(Notification, :count)
       end
     end
   end
@@ -112,17 +110,17 @@ RSpec.describe NotifyService, type: :service do
 
     it 'shows reblogs by default' do
       recipient.follow!(sender)
-      is_expected.to change(Notification, :count)
+      expect { subject }.to change(Notification, :count)
     end
 
     it 'shows reblogs when explicitly enabled' do
       recipient.follow!(sender, reblogs: true)
-      is_expected.to change(Notification, :count)
+      expect { subject }.to change(Notification, :count)
     end
 
     it 'shows reblogs when disabled' do
       recipient.follow!(sender, reblogs: false)
-      is_expected.to change(Notification, :count)
+      expect { subject }.to change(Notification, :count)
     end
   end
 
@@ -134,12 +132,12 @@ RSpec.describe NotifyService, type: :service do
 
     it 'does not notify when conversation is muted' do
       recipient.mute_conversation!(activity.status.conversation)
-      is_expected.to_not change(Notification, :count)
+      expect { subject }.to_not change(Notification, :count)
     end
 
     it 'does not notify when it is a reply to a blocked user' do
       recipient.block!(asshole)
-      is_expected.to_not change(Notification, :count)
+      expect { subject }.to_not change(Notification, :count)
     end
   end
 
@@ -147,7 +145,7 @@ RSpec.describe NotifyService, type: :service do
     let(:sender) { recipient }
 
     it 'does not notify when recipient is the sender' do
-      is_expected.to_not change(Notification, :count)
+      expect { subject }.to_not change(Notification, :count)
     end
   end
 
@@ -163,7 +161,7 @@ RSpec.describe NotifyService, type: :service do
       let(:enabled) { true }
 
       it 'sends email' do
-        is_expected.to change(ActionMailer::Base.deliveries, :count).by(1)
+        expect { subject }.to change(ActionMailer::Base.deliveries, :count).by(1)
       end
     end
 
@@ -171,7 +169,7 @@ RSpec.describe NotifyService, type: :service do
       let(:enabled) { false }
 
       it "doesn't send email" do
-        is_expected.to_not change(ActionMailer::Base.deliveries, :count).from(0)
+        expect { subject }.to_not change(ActionMailer::Base.deliveries, :count).from(0)
       end
     end
   end
diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb
index cf7eb257a..5d45e4ffd 100644
--- a/spec/services/suspend_account_service_spec.rb
+++ b/spec/services/suspend_account_service_spec.rb
@@ -5,9 +5,7 @@ RSpec.describe SuspendAccountService, type: :service do
     let!(:local_follower) { Fabricate(:user, current_sign_in_at: 1.hour.ago).account }
     let!(:list)           { Fabricate(:list, account: local_follower) }
 
-    subject do
-      -> { described_class.new.call(account) }
-    end
+    subject { described_class.new.call(account) }
 
     before do
       allow(FeedManager.instance).to receive(:unmerge_from_home).and_return(nil)
@@ -18,13 +16,13 @@ RSpec.describe SuspendAccountService, type: :service do
     end
 
     it "unmerges from local followers' feeds" do
-      subject.call
+      subject
       expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower)
       expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list)
     end
 
     it 'marks account as suspended' do
-      is_expected.to change { account.suspended? }.from(false).to(true)
+      expect { subject }.to change { account.suspended? }.from(false).to(true)
     end
   end
 
@@ -51,7 +49,7 @@ RSpec.describe SuspendAccountService, type: :service do
       end
 
       it 'sends an update actor to followers and reporters' do
-        subject.call
+        subject
         expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
         expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
       end
@@ -77,7 +75,7 @@ RSpec.describe SuspendAccountService, type: :service do
       end
 
       it 'sends a reject follow' do
-        subject.call
+        subject
         expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once
       end
     end
diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb
index 0593beb6f..3ac4cc085 100644
--- a/spec/services/unsuspend_account_service_spec.rb
+++ b/spec/services/unsuspend_account_service_spec.rb
@@ -5,9 +5,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
     let!(:local_follower) { Fabricate(:user, current_sign_in_at: 1.hour.ago).account }
     let!(:list)           { Fabricate(:list, account: local_follower) }
 
-    subject do
-      -> { described_class.new.call(account) }
-    end
+    subject { described_class.new.call(account) }
 
     before do
       allow(FeedManager.instance).to receive(:merge_into_home).and_return(nil)
@@ -33,7 +31,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
     end
 
     it 'marks account as unsuspended' do
-      is_expected.to change { account.suspended? }.from(true).to(false)
+      expect { subject }.to change { account.suspended? }.from(true).to(false)
     end
 
     include_examples 'common behavior' do
@@ -47,13 +45,13 @@ RSpec.describe UnsuspendAccountService, type: :service do
       end
 
       it "merges back into local followers' feeds" do
-        subject.call
+        subject
         expect(FeedManager.instance).to have_received(:merge_into_home).with(account, local_follower)
         expect(FeedManager.instance).to have_received(:merge_into_list).with(account, list)
       end
 
       it 'sends an update actor to followers and reporters' do
-        subject.call
+        subject
         expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
         expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
       end
@@ -75,18 +73,18 @@ RSpec.describe UnsuspendAccountService, type: :service do
         end
 
         it 're-fetches the account' do
-          subject.call
+          subject
           expect(resolve_account_service).to have_received(:call).with(account)
         end
 
         it "merges back into local followers' feeds" do
-          subject.call
+          subject
           expect(FeedManager.instance).to have_received(:merge_into_home).with(account, local_follower)
           expect(FeedManager.instance).to have_received(:merge_into_list).with(account, list)
         end
 
         it 'marks account as unsuspended' do
-          is_expected.to change { account.suspended? }.from(true).to(false)
+          expect { subject }.to change { account.suspended? }.from(true).to(false)
         end
       end
 
@@ -99,18 +97,18 @@ RSpec.describe UnsuspendAccountService, type: :service do
         end
 
         it 're-fetches the account' do
-          subject.call
+          subject
           expect(resolve_account_service).to have_received(:call).with(account)
         end
 
         it "does not merge back into local followers' feeds" do
-          subject.call
+          subject
           expect(FeedManager.instance).to_not have_received(:merge_into_home).with(account, local_follower)
           expect(FeedManager.instance).to_not have_received(:merge_into_list).with(account, list)
         end
 
         it 'does not mark the account as unsuspended' do
-          is_expected.not_to change { account.suspended? }
+          expect { subject }.not_to change { account.suspended? }
         end
       end
 
@@ -120,12 +118,12 @@ RSpec.describe UnsuspendAccountService, type: :service do
         end
 
         it 're-fetches the account' do
-          subject.call
+          subject
           expect(resolve_account_service).to have_received(:call).with(account)
         end
 
         it "does not merge back into local followers' feeds" do
-          subject.call
+          subject
           expect(FeedManager.instance).to_not have_received(:merge_into_home).with(account, local_follower)
           expect(FeedManager.instance).to_not have_received(:merge_into_list).with(account, list)
         end
diff --git a/spec/services/update_status_service_spec.rb b/spec/services/update_status_service_spec.rb
index 78cc89cd4..71a73be5b 100644
--- a/spec/services/update_status_service_spec.rb
+++ b/spec/services/update_status_service_spec.rb
@@ -3,6 +3,23 @@ require 'rails_helper'
 RSpec.describe UpdateStatusService, type: :service do
   subject { described_class.new }
 
+  context 'when nothing changes' do
+    let!(:status) { Fabricate(:status, text: 'Foo', language: 'en') }
+
+    before do
+      allow(ActivityPub::DistributionWorker).to receive(:perform_async)
+      subject.call(status, status.account_id, text: 'Foo')
+    end
+
+    it 'does not create an edit' do
+      expect(status.reload.edits).to be_empty
+    end
+
+    it 'does not notify anyone' do
+      expect(ActivityPub::DistributionWorker).to_not have_received(:perform_async)
+    end
+  end
+
   context 'when text changes' do
     let!(:status) { Fabricate(:status, text: 'Foo') }
     let(:preview_card) { Fabricate(:preview_card) }
diff --git a/spec/validators/status_length_validator_spec.rb b/spec/validators/status_length_validator_spec.rb
index 643ea6d22..4c80a59e6 100644
--- a/spec/validators/status_length_validator_spec.rb
+++ b/spec/validators/status_length_validator_spec.rb
@@ -55,6 +55,13 @@ describe StatusLengthValidator do
       expect(status.errors).to have_received(:add)
     end
 
+    it 'does not count overly long URLs as 23 characters flat' do
+      text = "http://example.com/valid?#{'#foo?' * 1000}"
+      status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false)
+      subject.validate(status)
+      expect(status.errors).to have_received(:add)
+    end
+
     it 'counts only the front part of remote usernames' do
       username = '@alice'
       chars = StatusLengthValidator::MAX_CHARS - 1 - username.length
@@ -64,5 +71,13 @@ describe StatusLengthValidator do
       subject.validate(status)
       expect(status.errors).to_not have_received(:add)
     end
+
+    it 'does count both parts of remote usernames for overly long domains' do
+      text   = "@alice@#{'b' * 500}.com"
+      status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false)
+
+      subject.validate(status)
+      expect(status.errors).to have_received(:add)
+    end
   end
 end