about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2023-01-17 11:41:05 -0600
committerStarfall <us@starfall.systems>2023-01-17 11:41:05 -0600
commit1f9c919b8769f5b0a3424ef343e0049d33d656e3 (patch)
tree1853486629da4b3b76192fe8756e8d4f6d71adcb /spec
parent957c21273ff42d5b2b4a5e16b7869bbb09aeb865 (diff)
parent13227e1dafd308dfe1a3effc3379b766274809b3 (diff)
Merge remote-tracking branch 'glitch/main'
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/domain_blocks_controller_spec.rb47
-rw-r--r--spec/controllers/admin/reports/actions_controller_spec.rb42
-rw-r--r--spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb47
-rw-r--r--spec/controllers/auth/passwords_controller_spec.rb61
-rw-r--r--spec/controllers/well_known/nodeinfo_controller_spec.rb2
-rw-r--r--spec/helpers/application_helper_spec.rb2
-rw-r--r--spec/helpers/formatting_helper_spec.rb24
-rw-r--r--spec/lib/feed_manager_spec.rb12
-rw-r--r--spec/lib/request_spec.rb5
-rw-r--r--spec/lib/sanitize_config_spec.rb4
-rw-r--r--spec/models/account_spec.rb8
-rw-r--r--spec/models/tag_spec.rb57
-rw-r--r--spec/presenters/account_relationships_presenter_spec.rb9
-rw-r--r--spec/services/suspend_account_service_spec.rb6
-rw-r--r--spec/services/unsuspend_account_service_spec.rb14
-rw-r--r--spec/services/verify_link_service_spec.rb27
-rw-r--r--spec/support/matchers/json/match_json_schema.rb6
-rw-r--r--spec/support/schema/nodeinfo_2.0.json170
18 files changed, 507 insertions, 36 deletions
diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb
index 98cda5004..f432060d9 100644
--- a/spec/controllers/admin/domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/domain_blocks_controller_spec.rb
@@ -70,6 +70,53 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
     end
   end
 
+  describe 'PUT #update' do
+    let!(:remote_account) { Fabricate(:account, domain: 'example.com') }
+    let(:domain_block)    { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) }
+
+    before do
+      BlockDomainService.new.call(domain_block)
+    end
+
+    let(:subject) do
+      post :update, params: { id: domain_block.id, domain_block: { domain: 'example.com', severity: new_severity } }
+    end
+
+    context 'downgrading a domain suspension to silence' do
+      let(:original_severity) { 'suspend' }
+      let(:new_severity)      { 'silence' }
+
+      it 'changes the block severity' do
+        expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence')
+      end
+
+      it 'undoes individual suspensions' do
+        expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false)
+      end
+
+      it 'performs individual silences' do
+        expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true)
+      end
+    end
+
+    context 'upgrading a domain silence to suspend' do
+      let(:original_severity) { 'silence' }
+      let(:new_severity)      { 'suspend' }
+
+      it 'changes the block severity' do
+        expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
+      end
+
+      it 'undoes individual silences' do
+        expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false)
+      end
+
+      it 'performs individual suspends' do
+        expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true)
+      end
+    end
+  end
+
   describe 'DELETE #destroy' do
     it 'unblocks the domain' do
       service = double(call: true)
diff --git a/spec/controllers/admin/reports/actions_controller_spec.rb b/spec/controllers/admin/reports/actions_controller_spec.rb
new file mode 100644
index 000000000..6609798dc
--- /dev/null
+++ b/spec/controllers/admin/reports/actions_controller_spec.rb
@@ -0,0 +1,42 @@
+require 'rails_helper'
+
+describe Admin::Reports::ActionsController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:account) { Fabricate(:account) }
+  let!(:status) { Fabricate(:status, account: account) }
+  let(:media_attached_status) { Fabricate(:status, account: account) }
+  let!(:media_attachment) { Fabricate(:media_attachment, account: account, status: media_attached_status) }
+  let(:media_attached_deleted_status) { Fabricate(:status, account: account, deleted_at: 1.day.ago) }
+  let!(:media_attachment2) { Fabricate(:media_attachment, account: account, status: media_attached_deleted_status) }
+  let(:last_media_attached_status) { Fabricate(:status, account: account) }
+  let!(:last_media_attachment) { Fabricate(:media_attachment, account: account, status: last_media_attached_status) }
+  let!(:last_status) { Fabricate(:status, account: account) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'POST #create' do
+    let(:report) { Fabricate(:report, status_ids: status_ids, account: user.account, target_account: account) }
+    let(:status_ids) { [media_attached_status.id, media_attached_deleted_status.id] }
+
+    before do
+      post :create, params: { report_id: report.id, action => '' }
+    end
+
+    context 'when action is mark_as_sensitive' do
+
+      let(:action) { 'mark_as_sensitive' }
+
+      it 'resolves the report' do
+        expect(report.reload.action_taken_at).to_not be_nil
+      end
+
+      it 'marks the non-deleted as sensitive' do
+        expect(media_attached_status.reload.sensitive).to eq true
+      end
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb
index f12285b2a..606def602 100644
--- a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb
@@ -71,6 +71,53 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
     end
   end
 
+  describe 'PUT #update' do
+    let!(:remote_account) { Fabricate(:account, domain: 'example.com') }
+    let(:domain_block)    { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) }
+
+    before do
+      BlockDomainService.new.call(domain_block)
+    end
+
+    let(:subject) do
+      post :update, params: { id: domain_block.id, domain: 'example.com', severity: new_severity }
+    end
+
+    context 'downgrading a domain suspension to silence' do
+      let(:original_severity) { 'suspend' }
+      let(:new_severity)      { 'silence' }
+
+      it 'changes the block severity' do
+        expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence')
+      end
+
+      it 'undoes individual suspensions' do
+        expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false)
+      end
+
+      it 'performs individual silences' do
+        expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true)
+      end
+    end
+
+    context 'upgrading a domain silence to suspend' do
+      let(:original_severity) { 'silence' }
+      let(:new_severity)      { 'suspend' }
+
+      it 'changes the block severity' do
+        expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
+      end
+
+      it 'undoes individual silences' do
+        expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false)
+      end
+
+      it 'performs individual suspends' do
+        expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true)
+      end
+    end
+  end
+
   describe 'DELETE #destroy' do
     let!(:block) { Fabricate(:domain_block) }
 
diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb
index dcfdebb17..1c6874f08 100644
--- a/spec/controllers/auth/passwords_controller_spec.rb
+++ b/spec/controllers/auth/passwords_controller_spec.rb
@@ -35,4 +35,65 @@ describe Auth::PasswordsController, type: :controller do
       end
     end
   end
+
+  describe 'POST #update' do
+    let(:user) { Fabricate(:user) }
+
+    before do
+      @password = 'reset0password'
+      request.env['devise.mapping'] = Devise.mappings[:user]
+    end
+
+    context 'with valid reset_password_token' do
+      let!(:session_activation) { Fabricate(:session_activation, user: user) }
+      let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
+      let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
+
+      before do
+        @token = user.send_reset_password_instructions
+
+        post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: @token } }
+      end
+
+      it 'redirect to sign in' do
+        expect(response).to redirect_to '/auth/sign_in'
+      end
+
+      it 'changes password' do
+        this_user = User.find(user.id)
+
+        expect(this_user).to_not be_nil
+        expect(this_user.valid_password?(@password)).to be true
+      end
+
+      it 'deactivates all sessions' do
+        expect(user.session_activations.count).to eq 0
+      end
+
+      it 'revokes all access tokens' do
+        expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0
+      end
+
+      it 'removes push subscriptions' do
+        expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
+      end
+    end
+
+    context 'with invalid reset_password_token' do
+      before do
+        post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: 'some_invalid_value' } }
+      end
+
+      it 'renders reset password' do
+        expect(response).to render_template(:new)
+      end
+
+      it 'retains password' do
+        this_user = User.find(user.id)
+
+        expect(this_user).to_not be_nil
+        expect(this_user.external_or_valid_password?(user.password)).to be true
+      end
+    end
+  end
 end
diff --git a/spec/controllers/well_known/nodeinfo_controller_spec.rb b/spec/controllers/well_known/nodeinfo_controller_spec.rb
index 694bb0fb9..36e85f20d 100644
--- a/spec/controllers/well_known/nodeinfo_controller_spec.rb
+++ b/spec/controllers/well_known/nodeinfo_controller_spec.rb
@@ -27,6 +27,8 @@ describe WellKnown::NodeInfoController, type: :controller do
 
       json = body_as_json
 
+      expect({ "foo" => 0 }).not_to match_json_schema("nodeinfo_2.0")
+      expect(json).to match_json_schema("nodeinfo_2.0")
       expect(json[:version]).to eq '2.0'
       expect(json[:usage]).to be_a Hash
       expect(json[:software]).to be_a Hash
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 20ee32aa0..1dbd985bf 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -113,7 +113,7 @@ describe ApplicationHelper do
       Setting.site_title = site_title
     end
 
-    it 'returns site title on production enviroment' do
+    it 'returns site title on production environment' do
       Setting.site_title = 'site title'
       expect(Rails.env).to receive(:production?).and_return(true)
       expect(helper.title).to eq 'site title'
diff --git a/spec/helpers/formatting_helper_spec.rb b/spec/helpers/formatting_helper_spec.rb
new file mode 100644
index 000000000..af604a87b
--- /dev/null
+++ b/spec/helpers/formatting_helper_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe FormattingHelper, type: :helper do
+  include Devise::Test::ControllerHelpers
+
+  describe '#rss_status_content_format' do
+    let(:status) { Fabricate(:status, text: 'Hello world<>', spoiler_text: 'This is a spoiler<>', poll: Fabricate(:poll, options: %w(Yes<> No))) }
+    let(:html) { helper.rss_status_content_format(status) }
+
+    it 'renders the spoiler text' do
+      expect(html).to include('<p>This is a spoiler&lt;&gt;</p><hr>')
+    end
+
+    it 'renders the status text' do
+      expect(html).to include('<p>Hello world&lt;&gt;</p>')
+    end
+
+    it 'renders the poll' do
+      expect(html).to include('<radio disabled="disabled">Yes&lt;&gt;</radio><br>')
+    end
+  end
+end
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index 2b11ddf70..f2ab2570d 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -39,6 +39,18 @@ RSpec.describe FeedManager do
         expect(FeedManager.instance.filter?(:home, reblog, bob)).to be false
       end
 
+      it 'returns true for post from account who blocked me' do
+        status = Fabricate(:status, text: 'Hello, World', account: alice)
+        alice.block!(bob)
+        expect(FeedManager.instance.filter?(:home, status, bob)).to be true
+      end
+
+      it 'returns true for post from blocked account' do
+        status = Fabricate(:status, text: 'Hello, World', account: alice)
+        bob.block!(alice)
+        expect(FeedManager.instance.filter?(:home, status, bob)).to be true
+      end
+
       it 'returns true for reblog by followee of blocked account' do
         status = Fabricate(:status, text: 'Hello world', account: jeff)
         reblog = Fabricate(:status, reblog: status, account: alice)
diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb
index 5eccf3201..8539944e2 100644
--- a/spec/lib/request_spec.rb
+++ b/spec/lib/request_spec.rb
@@ -120,6 +120,11 @@ describe Request do
       expect { subject.perform { |response| response.body_with_limit } }.to raise_error Mastodon::LengthValidationError
     end
 
+    it 'truncates large monolithic body' do
+      stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Content-Length' => 2.megabytes })
+      expect(subject.perform { |response| response.truncated_body.bytesize }).to be < 2.megabytes
+    end
+
     it 'uses binary encoding if Content-Type does not tell encoding' do
       stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html' })
       expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::BINARY
diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb
index dc6418e5b..29344476f 100644
--- a/spec/lib/sanitize_config_spec.rb
+++ b/spec/lib/sanitize_config_spec.rb
@@ -28,6 +28,10 @@ describe Sanitize::Config do
       expect(Sanitize.fragment('<a href="foo://bar">Test</a>', subject)).to eq 'Test'
     end
 
+    it 'does not re-interpret HTML when removing unsupported links' do
+      expect(Sanitize.fragment('<a href="foo://bar">Test&lt;a href="https://example.com"&gt;test&lt;/a&gt;</a>', subject)).to eq 'Test&lt;a href="https://example.com"&gt;test&lt;/a&gt;'
+    end
+
     it 'keeps a with href' do
       expect(Sanitize.fragment('<a href="http://example.com">Test</a>', subject)).to eq '<a href="http://example.com" rel="nofollow noopener noreferrer" target="_blank">Test</a>'
     end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index edae05f9d..6cd769dc8 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -160,7 +160,7 @@ RSpec.describe Account, type: :model do
         expect(account.avatar_remote_url).to eq 'https://remote.test/invalid_avatar'
         expect(account.header_remote_url).to eq expectation.header_remote_url
         expect(account.avatar_file_name).to  eq nil
-        expect(account.header_file_name).to  eq nil
+        expect(account.header_file_name).to  eq expectation.header_file_name
       end
     end
   end
@@ -658,6 +658,12 @@ RSpec.describe Account, type: :model do
     end
   end
 
+  describe '.requested_by_map' do
+    it 'returns an hash' do
+      expect(Account.requested_by_map([], 1)).to be_a Hash
+    end
+  end
+
   describe 'MENTION_RE' do
     subject { Account::MENTION_RE }
 
diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb
index b16f99a79..102d2f625 100644
--- a/spec/models/tag_spec.rb
+++ b/spec/models/tag_spec.rb
@@ -1,21 +1,22 @@
+# frozen_string_literal: true
 require 'rails_helper'
 
-RSpec.describe Tag, type: :model do
+RSpec.describe Tag do
   describe 'validations' do
     it 'invalid with #' do
-      expect(Tag.new(name: '#hello_world')).to_not be_valid
+      expect(described_class.new(name: '#hello_world')).not_to be_valid
     end
 
     it 'invalid with .' do
-      expect(Tag.new(name: '.abcdef123')).to_not be_valid
+      expect(described_class.new(name: '.abcdef123')).not_to be_valid
     end
 
     it 'invalid with spaces' do
-      expect(Tag.new(name: 'hello world')).to_not be_valid
+      expect(described_class.new(name: 'hello world')).not_to be_valid
     end
 
     it 'valid with aesthetic' do
-      expect(Tag.new(name: 'aesthetic')).to be_valid
+      expect(described_class.new(name: 'aesthetic')).to be_valid
     end
   end
 
@@ -62,6 +63,10 @@ RSpec.describe Tag, type: :model do
       expect(subject.match('hello #one·two·three').to_s).to eq ' #one·two·three'
     end
 
+    it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do
+      expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq ' #ぼっち・ざ・ろっく'
+    end
+
     it 'matches ZWNJ' do
       expect(subject.match('just add #نرم‌افزار and').to_s).to eq ' #نرم‌افزار'
     end
@@ -89,44 +94,46 @@ RSpec.describe Tag, type: :model do
   describe '.find_normalized' do
     it 'returns tag for a multibyte case-insensitive name' do
       upcase_string   = 'abcABCabcABCやゆよ'
-      downcase_string = 'abcabcabcabcやゆよ';
+      downcase_string = 'abcabcabcabcやゆよ'
 
       tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string))
-      expect(Tag.find_normalized(upcase_string)).to eq tag
+      expect(described_class.find_normalized(upcase_string)).to eq tag
     end
   end
 
   describe '.matches_name' do
     it 'returns tags for multibyte case-insensitive names' do
       upcase_string   = 'abcABCabcABCやゆよ'
-      downcase_string = 'abcabcabcabcやゆよ';
+      downcase_string = 'abcabcabcabcやゆよ'
 
       tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string))
-      expect(Tag.matches_name(upcase_string)).to eq [tag]
+      expect(described_class.matches_name(upcase_string)).to eq [tag]
     end
 
     it 'uses the LIKE operator' do
-      expect(Tag.matches_name('100%abc').to_sql).to eq %q[SELECT "tags".* FROM "tags" WHERE LOWER("tags"."name") LIKE LOWER('100abc%')]
+      result = %q[SELECT "tags".* FROM "tags" WHERE LOWER("tags"."name") LIKE LOWER('100abc%')]
+      expect(described_class.matches_name('100%abc').to_sql).to eq result
     end
   end
 
   describe '.matching_name' do
     it 'returns tags for multibyte case-insensitive names' do
       upcase_string   = 'abcABCabcABCやゆよ'
-      downcase_string = 'abcabcabcabcやゆよ';
+      downcase_string = 'abcabcabcabcやゆよ'
 
       tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string))
-      expect(Tag.matching_name(upcase_string)).to eq [tag]
+      expect(described_class.matching_name(upcase_string)).to eq [tag]
     end
   end
 
   describe '.find_or_create_by_names' do
+    let(:upcase_string) { 'abcABCabcABCやゆよ' }
+    let(:downcase_string) { 'abcabcabcabcやゆよ' }
+
     it 'runs a passed block once per tag regardless of duplicates' do
-      upcase_string   = 'abcABCabcABCやゆよ'
-      downcase_string = 'abcabcabcabcやゆよ';
-      count           = 0
+      count = 0
 
-      Tag.find_or_create_by_names([upcase_string, downcase_string]) do |tag|
+      described_class.find_or_create_by_names([upcase_string, downcase_string]) do |_tag|
         count += 1
       end
 
@@ -136,28 +143,28 @@ RSpec.describe Tag, type: :model do
 
   describe '.search_for' do
     it 'finds tag records with matching names' do
-      tag = Fabricate(:tag, name: "match")
-      _miss_tag = Fabricate(:tag, name: "miss")
+      tag = Fabricate(:tag, name: 'match')
+      _miss_tag = Fabricate(:tag, name: 'miss')
 
-      results = Tag.search_for("match")
+      results = described_class.search_for('match')
 
       expect(results).to eq [tag]
     end
 
     it 'finds tag records in case insensitive' do
-      tag = Fabricate(:tag, name: "MATCH")
-      _miss_tag = Fabricate(:tag, name: "miss")
+      tag = Fabricate(:tag, name: 'MATCH')
+      _miss_tag = Fabricate(:tag, name: 'miss')
 
-      results = Tag.search_for("match")
+      results = described_class.search_for('match')
 
       expect(results).to eq [tag]
     end
 
     it 'finds the exact matching tag as the first item' do
-      similar_tag = Fabricate(:tag, name: "matchlater", reviewed_at: Time.now.utc)
-      tag = Fabricate(:tag, name: "match", reviewed_at: Time.now.utc)
+      similar_tag = Fabricate(:tag, name: 'matchlater', reviewed_at: Time.now.utc)
+      tag = Fabricate(:tag, name: 'match', reviewed_at: Time.now.utc)
 
-      results = Tag.search_for("match")
+      results = described_class.search_for('match')
 
       expect(results).to eq [tag, similar_tag]
     end
diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb
index edfbbb354..8a485d2b9 100644
--- a/spec/presenters/account_relationships_presenter_spec.rb
+++ b/spec/presenters/account_relationships_presenter_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe AccountRelationshipsPresenter do
       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(:requested_by_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
 
@@ -71,6 +72,14 @@ RSpec.describe AccountRelationshipsPresenter do
       end
     end
 
+    context 'options[:requested_by_map] is set' do
+      let(:options) { { requested_by_map: { 6 => true } } }
+
+      it 'sets @requested merged with default_map and options[:requested_by_map]' do
+        expect(presenter.requested_by).to eq default_map.merge(options[:requested_by_map])
+      end
+    end
+
     context 'options[:domain_blocking_map] is set' do
       let(:options) { { domain_blocking_map: { 7 => true } } }
 
diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb
index 5d45e4ffd..126b13986 100644
--- a/spec/services/suspend_account_service_spec.rb
+++ b/spec/services/suspend_account_service_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe SuspendAccountService, type: :service do
 
       local_follower.follow!(account)
       list.accounts << account
+
+      account.suspend!
     end
 
     it "unmerges from local followers' feeds" do
@@ -21,8 +23,8 @@ RSpec.describe SuspendAccountService, type: :service do
       expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list)
     end
 
-    it 'marks account as suspended' do
-      expect { subject }.to change { account.suspended? }.from(false).to(true)
+    it 'does not change the “suspended” flag' do
+      expect { subject }.to_not change { account.suspended? }
     end
   end
 
diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb
index 3ac4cc085..987eb09e2 100644
--- a/spec/services/unsuspend_account_service_spec.rb
+++ b/spec/services/unsuspend_account_service_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
       local_follower.follow!(account)
       list.accounts << account
 
-      account.suspend!(origin: :local)
+      account.unsuspend!
     end
   end
 
@@ -30,8 +30,8 @@ RSpec.describe UnsuspendAccountService, type: :service do
       stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
     end
 
-    it 'marks account as unsuspended' do
-      expect { subject }.to change { account.suspended? }.from(true).to(false)
+    it 'does not change the “suspended” flag' do
+      expect { subject }.to_not change { account.suspended? }
     end
 
     include_examples 'common behavior' do
@@ -83,8 +83,8 @@ RSpec.describe UnsuspendAccountService, type: :service do
           expect(FeedManager.instance).to have_received(:merge_into_list).with(account, list)
         end
 
-        it 'marks account as unsuspended' do
-          expect { subject }.to change { account.suspended? }.from(true).to(false)
+        it 'does not change the “suspended” flag' do
+          expect { subject }.to_not change { account.suspended? }
         end
       end
 
@@ -107,8 +107,8 @@ RSpec.describe UnsuspendAccountService, type: :service do
           expect(FeedManager.instance).to_not have_received(:merge_into_list).with(account, list)
         end
 
-        it 'does not mark the account as unsuspended' do
-          expect { subject }.not_to change { account.suspended? }
+        it 'marks account as suspended' do
+          expect { subject }.to change { account.suspended? }.from(false).to(true)
         end
       end
 
diff --git a/spec/services/verify_link_service_spec.rb b/spec/services/verify_link_service_spec.rb
index 52ba454cc..391560f1c 100644
--- a/spec/services/verify_link_service_spec.rb
+++ b/spec/services/verify_link_service_spec.rb
@@ -73,6 +73,33 @@ RSpec.describe VerifyLinkService, type: :service do
       end
     end
 
+    context 'when a document is truncated but the link back is valid' do
+      let(:html) do
+        "
+          <!doctype html>
+          <body>
+            <a rel=\"me\" href=\"#{ActivityPub::TagManager.instance.url_for(account)}\"
+        "
+      end
+
+      it 'marks the field as not verified' do
+        expect(field.verified?).to be false
+      end
+    end
+
+    context 'when a link back might be truncated' do
+      let(:html) do
+        "
+          <!doctype html>
+          <body>
+            <a rel=\"me\" href=\"#{ActivityPub::TagManager.instance.url_for(account)}"
+      end
+
+      it 'does not mark the field as verified' do
+        expect(field.verified?).to be false
+      end
+    end
+
     context 'when a link does not contain a link back' do
       let(:html) { '' }
 
diff --git a/spec/support/matchers/json/match_json_schema.rb b/spec/support/matchers/json/match_json_schema.rb
new file mode 100644
index 000000000..5d9c9a618
--- /dev/null
+++ b/spec/support/matchers/json/match_json_schema.rb
@@ -0,0 +1,6 @@
+RSpec::Matchers.define :match_json_schema do |schema|
+  match do |input_json|
+    schema_path = Rails.root.join('spec', 'support', 'schema', "#{schema}.json").to_s
+    JSON::Validator.validate(schema_path, input_json, validate_schema: true)
+  end
+end
diff --git a/spec/support/schema/nodeinfo_2.0.json b/spec/support/schema/nodeinfo_2.0.json
new file mode 100644
index 000000000..085ce542b
--- /dev/null
+++ b/spec/support/schema/nodeinfo_2.0.json
@@ -0,0 +1,170 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "http://nodeinfo.diaspora.software/ns/schema/2.0#",
+  "description": "NodeInfo schema version 2.0.",
+  "type": "object",
+  "additionalProperties": false,
+  "required": [
+    "version",
+    "software",
+    "protocols",
+    "services",
+    "openRegistrations",
+    "usage",
+    "metadata"
+  ],
+  "properties": {
+    "version": {
+      "description": "The schema version, must be 2.0.",
+      "enum": ["2.0"]
+    },
+    "software": {
+      "description": "Metadata about server software in use.",
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["name", "version"],
+      "properties": {
+        "name": {
+          "description": "The canonical name of this server software.",
+          "type": "string",
+          "pattern": "^[a-z0-9-]+$"
+        },
+        "version": {
+          "description": "The version of this server software.",
+          "type": "string"
+        }
+      }
+    },
+    "protocols": {
+      "description": "The protocols supported on this server.",
+      "type": "array",
+      "minItems": 1,
+      "items": {
+        "enum": [
+          "activitypub",
+          "buddycloud",
+          "dfrn",
+          "diaspora",
+          "libertree",
+          "ostatus",
+          "pumpio",
+          "tent",
+          "xmpp",
+          "zot"
+        ]
+      }
+    },
+    "services": {
+      "description": "The third party sites this server can connect to via their application API.",
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["inbound", "outbound"],
+      "properties": {
+        "inbound": {
+          "description": "The third party sites this server can retrieve messages from for combined display with regular traffic.",
+          "type": "array",
+          "minItems": 0,
+          "items": {
+            "enum": [
+              "atom1.0",
+              "gnusocial",
+              "imap",
+              "pnut",
+              "pop3",
+              "pumpio",
+              "rss2.0",
+              "twitter"
+            ]
+          }
+        },
+        "outbound": {
+          "description": "The third party sites this server can publish messages to on the behalf of a user.",
+          "type": "array",
+          "minItems": 0,
+          "items": {
+            "enum": [
+              "atom1.0",
+              "blogger",
+              "buddycloud",
+              "diaspora",
+              "dreamwidth",
+              "drupal",
+              "facebook",
+              "friendica",
+              "gnusocial",
+              "google",
+              "insanejournal",
+              "libertree",
+              "linkedin",
+              "livejournal",
+              "mediagoblin",
+              "myspace",
+              "pinterest",
+              "pnut",
+              "posterous",
+              "pumpio",
+              "redmatrix",
+              "rss2.0",
+              "smtp",
+              "tent",
+              "tumblr",
+              "twitter",
+              "wordpress",
+              "xmpp"
+            ]
+          }
+        }
+      }
+    },
+    "openRegistrations": {
+      "description": "Whether this server allows open self-registration.",
+      "type": "boolean"
+    },
+    "usage": {
+      "description": "Usage statistics for this server.",
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["users"],
+      "properties": {
+        "users": {
+          "description": "statistics about the users of this server.",
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "total": {
+              "description": "The total amount of on this server registered users.",
+              "type": "integer",
+              "minimum": 0
+            },
+            "activeHalfyear": {
+              "description": "The amount of users that signed in at least once in the last 180 days.",
+              "type": "integer",
+              "minimum": 0
+            },
+            "activeMonth": {
+              "description": "The amount of users that signed in at least once in the last 30 days.",
+              "type": "integer",
+              "minimum": 0
+            }
+          }
+        },
+        "localPosts": {
+          "description": "The amount of posts that were made by users that are registered on this server.",
+          "type": "integer",
+          "minimum": 0
+        },
+        "localComments": {
+          "description": "The amount of comments that were made by users that are registered on this server.",
+          "type": "integer",
+          "minimum": 0
+        }
+      }
+    },
+    "metadata": {
+      "description": "Free form key value pairs for software specific values. Clients should not rely on any specific key present.",
+      "type": "object",
+      "minProperties": 0,
+      "additionalProperties": true
+    }
+  }
+}