about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2020-01-04 22:54:06 +0100
committerThibaut Girka <thib@sitedethib.com>2020-01-04 23:04:42 +0100
commit01eaeab56df4da4c697b1096f40a400cc9e2b8e8 (patch)
tree6288ee106b4615cacd98362f9fd268317a9cdeff /spec
parent22daf24600d8e99e4569740ee5836d25c70c1e8b (diff)
parent2ecc7802caf4d272191a7fd582fc97996f750827 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- `app/controllers/application_controller.rb`:
  Conflict due to theming system.
- `app/controllers/oauth/authorizations_controller.rb`:
  Conflict due to theming system.
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/api/proofs_controller_spec.rb5
-rw-r--r--spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb27
-rw-r--r--spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb27
-rw-r--r--spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb20
-rw-r--r--spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb20
-rw-r--r--spec/controllers/concerns/obfuscate_filename_spec.rb30
-rw-r--r--spec/controllers/follower_accounts_controller_spec.rb12
-rw-r--r--spec/controllers/following_accounts_controller_spec.rb12
-rw-r--r--spec/features/log_in_spec.rb36
-rw-r--r--spec/features/profile_spec.rb53
-rw-r--r--spec/middleware/handle_bad_encoding_middleware_spec.rb21
-rw-r--r--spec/models/account_spec.rb1
-rw-r--r--spec/models/media_attachment_spec.rb18
-rw-r--r--spec/services/process_mentions_service_spec.rb46
-rw-r--r--spec/support/examples/models/concerns/account_avatar.rb20
-rw-r--r--spec/support/examples/models/concerns/account_header.rb23
-rw-r--r--spec/support/stories/profile_stories.rb45
-rw-r--r--spec/workers/refollow_worker_spec.rb30
18 files changed, 368 insertions, 78 deletions
diff --git a/spec/controllers/api/proofs_controller_spec.rb b/spec/controllers/api/proofs_controller_spec.rb
index dbde4927f..2fe615005 100644
--- a/spec/controllers/api/proofs_controller_spec.rb
+++ b/spec/controllers/api/proofs_controller_spec.rb
@@ -85,10 +85,7 @@ describe Api::ProofsController do
         end
 
         it 'has the correct avatar url' do
-          first_part = 'https://cb6e6126.ngrok.io/system/accounts/avatars/'
-          last_part  = 'original/avatar.gif'
-
-          expect(body_as_json[:avatar]).to match /#{Regexp.quote(first_part)}(?:\d{3,5}\/){3}#{Regexp.quote(last_part)}/
+          expect(body_as_json[:avatar]).to match "https://cb6e6126.ngrok.io#{alice.avatar.url}"
         end
       end
     end
diff --git a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
index 75e0570e9..54587187f 100644
--- a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
@@ -3,19 +3,38 @@ require 'rails_helper'
 describe Api::V1::Accounts::FollowerAccountsController do
   render_views
 
-  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:user)    { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+  let(:alice)   { Fabricate(:account) }
+  let(:bob)     { Fabricate(:account) }
 
   before do
-    Fabricate(:follow, target_account: user.account)
+    alice.follow!(account)
+    bob.follow!(account)
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
   describe 'GET #index' do
     it 'returns http success' do
-      get :index, params: { account_id: user.account.id, limit: 1 }
+      get :index, params: { account_id: account.id, limit: 2 }
 
       expect(response).to have_http_status(200)
     end
+
+    it 'returns accounts following the given account' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(body_as_json.size).to eq 2
+      expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
+    end
+
+    it 'does not return blocked users' do
+      user.account.block!(bob)
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(body_as_json.size).to eq 1
+      expect(body_as_json[0][:id]).to eq alice.id.to_s
+    end
   end
 end
diff --git a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
index 7f7105ad3..a580a7368 100644
--- a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
@@ -3,19 +3,38 @@ require 'rails_helper'
 describe Api::V1::Accounts::FollowingAccountsController do
   render_views
 
-  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:user)    { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+  let(:alice)   { Fabricate(:account) }
+  let(:bob)     { Fabricate(:account) }
 
   before do
-    Fabricate(:follow, account: user.account)
+    account.follow!(alice)
+    account.follow!(bob)
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
   describe 'GET #index' do
     it 'returns http success' do
-      get :index, params: { account_id: user.account.id, limit: 1 }
+      get :index, params: { account_id: account.id, limit: 2 }
 
       expect(response).to have_http_status(200)
     end
+
+    it 'returns accounts followed by the given account' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(body_as_json.size).to eq 2
+      expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
+    end
+
+    it 'does not return blocked users' do
+      user.account.block!(bob)
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(body_as_json.size).to eq 1
+      expect(body_as_json[0][:id]).to eq alice.id.to_s
+    end
   end
 end
diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
index 40f75c700..f053ae573 100644
--- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
   let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') }
+  let(:alice) { Fabricate(:account) }
+  let(:bob)   { Fabricate(:account) }
 
   context 'with an oauth token' do
     before do
@@ -16,14 +18,28 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
       let(:status) { Fabricate(:status, account: user.account) }
 
       before do
-        Fabricate(:favourite, status: status)
+        Favourite.create!(account: alice, status: status)
+        Favourite.create!(account: bob, status: status)
       end
 
       it 'returns http success' do
-        get :index, params: { status_id: status.id, limit: 1 }
+        get :index, params: { status_id: status.id, limit: 2 }
         expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
+
+      it 'returns accounts who favorited the status' do
+        get :index, params: { status_id: status.id, limit: 2 }
+        expect(body_as_json.size).to eq 2
+      expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
+      end
+
+      it 'does not return blocked users' do
+        user.account.block!(bob)
+        get :index, params: { status_id: status.id, limit: 2 }
+        expect(body_as_json.size).to eq 1
+        expect(body_as_json[0][:id]).to eq alice.id.to_s
+      end
     end
   end
 
diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
index d758786dc..60908b7b3 100644
--- a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
   let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') }
+  let(:alice) { Fabricate(:account) }
+  let(:bob)   { Fabricate(:account) }
 
   context 'with an oauth token' do
     before do
@@ -16,14 +18,28 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
       let(:status) { Fabricate(:status, account: user.account) }
 
       before do
-        Fabricate(:status, reblog_of_id: status.id)
+        Fabricate(:status, account: alice, reblog_of_id: status.id)
+        Fabricate(:status, account: bob, reblog_of_id: status.id)
       end
 
       it 'returns http success' do
-        get :index, params: { status_id: status.id, limit: 1 }
+        get :index, params: { status_id: status.id, limit: 2 }
         expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
+
+      it 'returns accounts who reblogged the status' do
+        get :index, params: { status_id: status.id, limit: 2 }
+        expect(body_as_json.size).to eq 2
+      expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
+      end
+
+      it 'does not return blocked users' do
+        user.account.block!(bob)
+        get :index, params: { status_id: status.id, limit: 2 }
+        expect(body_as_json.size).to eq 1
+        expect(body_as_json[0][:id]).to eq alice.id.to_s
+      end
     end
   end
 
diff --git a/spec/controllers/concerns/obfuscate_filename_spec.rb b/spec/controllers/concerns/obfuscate_filename_spec.rb
deleted file mode 100644
index e06d53c03..000000000
--- a/spec/controllers/concerns/obfuscate_filename_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe ApplicationController, type: :controller do
-  controller do
-    include ObfuscateFilename
-
-    obfuscate_filename :file
-
-    def file
-      render plain: params[:file]&.original_filename
-    end
-  end
-
-  before do
-    routes.draw { get 'file' => 'anonymous#file' }
-  end
-
-  it 'obfusticates filename if the given parameter is specified' do
-    file = fixture_file_upload('files/imports.txt', 'text/plain')
-    post 'file', params: { file: file }
-    expect(response.body).to end_with '.txt'
-    expect(response.body).not_to include 'imports'
-  end
-
-  it 'does nothing if the given parameter is not specified' do
-    post 'file'
-  end
-end
diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb
index 83032ab64..34a0cf3f4 100644
--- a/spec/controllers/follower_accounts_controller_spec.rb
+++ b/spec/controllers/follower_accounts_controller_spec.rb
@@ -22,6 +22,18 @@ describe FollowerAccountsController do
         expect(assigned[0]).to eq follow1
         expect(assigned[1]).to eq follow0
       end
+
+      it 'does not assign blocked users' do
+        user = Fabricate(:user)
+        user.account.block!(follower0)
+        sign_in(user)
+
+        expect(response).to have_http_status(200)
+
+        assigned = assigns(:follows).to_a
+        expect(assigned.size).to eq 1
+        expect(assigned[0]).to eq follow1
+      end
     end
 
     context 'when format is json' do
diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb
index d5e4ee587..e9a1f597d 100644
--- a/spec/controllers/following_accounts_controller_spec.rb
+++ b/spec/controllers/following_accounts_controller_spec.rb
@@ -22,6 +22,18 @@ describe FollowingAccountsController do
         expect(assigned[0]).to eq follow1
         expect(assigned[1]).to eq follow0
       end
+
+      it 'does not assign blocked users' do
+        user = Fabricate(:user)
+        user.account.block!(followee0)
+        sign_in(user)
+
+        expect(response).to have_http_status(200)
+
+        assigned = assigns(:follows).to_a
+        expect(assigned.size).to eq 1
+        expect(assigned[0]).to eq follow1
+      end
     end
 
     context 'when format is json' do
diff --git a/spec/features/log_in_spec.rb b/spec/features/log_in_spec.rb
index b874c255b..de1a6de03 100644
--- a/spec/features/log_in_spec.rb
+++ b/spec/features/log_in_spec.rb
@@ -1,47 +1,51 @@
-require "rails_helper"
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+feature 'Log in' do
+  include ProfileStories
 
-feature "Log in" do
   given(:email)        { "test@example.com" }
   given(:password)     { "password" }
   given(:confirmed_at) { Time.zone.now }
 
   background do
-    Fabricate(:user, email: email, password: password, confirmed_at: confirmed_at)
+    as_a_registered_user
     visit new_user_session_path
   end
 
   subject { page }
 
-  scenario "A valid email and password user is able to log in" do
-    fill_in "user_email", with: email
-    fill_in "user_password", with: password
+  scenario 'A valid email and password user is able to log in' do
+    fill_in 'user_email', with: email
+    fill_in 'user_password', with: password
     click_on I18n.t('auth.login')
 
-    is_expected.to have_css("div.app-holder")
+    is_expected.to have_css('div.app-holder')
   end
 
-  scenario "A invalid email and password user is not able to log in" do
-    fill_in "user_email", with: "invalid_email"
-    fill_in "user_password", with: "invalid_password"
+  scenario 'A invalid email and password user is not able to log in' do
+    fill_in 'user_email', with: 'invalid_email'
+    fill_in 'user_password', with: 'invalid_password'
     click_on I18n.t('auth.login')
 
-    is_expected.to have_css(".flash-message", text: failure_message("invalid"))
+    is_expected.to have_css('.flash-message', text: failure_message('invalid'))
   end
 
   context do
     given(:confirmed_at) { nil }
 
-    scenario "A unconfirmed user is able to log in" do
-      fill_in "user_email", with: email
-      fill_in "user_password", with: password
+    scenario 'A unconfirmed user is able to log in' do
+      fill_in 'user_email', with: email
+      fill_in 'user_password', with: password
       click_on I18n.t('auth.login')
 
-      is_expected.to have_css("div.admin-wrapper")
+      is_expected.to have_css('div.admin-wrapper')
     end
   end
 
   def failure_message(message)
     keys = User.authentication_keys.map { |key| User.human_attribute_name(key) }
-    I18n.t("devise.failure.#{message}", authentication_keys: keys.join("support.array.words_connector"))
+    I18n.t("devise.failure.#{message}", authentication_keys: keys.join('support.array.words_connector'))
   end
 end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
new file mode 100644
index 000000000..3202167ca
--- /dev/null
+++ b/spec/features/profile_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+feature 'Profile' do
+  include ProfileStories
+
+  given(:local_domain) { ENV['LOCAL_DOMAIN'] }
+
+  background do
+    as_a_logged_in_user
+    with_alice_as_local_user
+  end
+
+  subject { page }
+
+  scenario 'I can view Annes public account' do
+    visit account_path('alice')
+
+    is_expected.to have_title("alice (@alice@#{local_domain})")
+
+    within('.public-account-header h1') do
+      is_expected.to have_content("alice @alice@#{local_domain}")
+    end
+
+    bio_elem = first('.public-account-bio')
+    expect(bio_elem).to have_content(alice_bio)
+    # The bio has hashtags made clickable
+    expect(bio_elem).to have_link('cryptology')
+    expect(bio_elem).to have_link('science')
+    # Nicknames are make clickable
+    expect(bio_elem).to have_link('@alice')
+    expect(bio_elem).to have_link('@bob')
+    # Nicknames not on server are not clickable
+    expect(bio_elem).not_to have_link('@pepe')
+  end
+
+  scenario 'I can change my account' do
+    visit settings_profile_path
+    fill_in 'Display name', with: 'Bob'
+    fill_in 'Bio', with: 'Bob is silent'
+    click_on 'Save changes'
+    is_expected.to have_content 'Changes successfully saved!'
+
+    # View my own public profile and see the changes
+    click_link "Bob @bob@#{local_domain}"
+
+    within('.public-account-header h1') do
+      is_expected.to have_content("Bob @bob@#{local_domain}")
+    end
+    expect(first('.public-account-bio')).to have_content('Bob is silent')
+  end
+end
diff --git a/spec/middleware/handle_bad_encoding_middleware_spec.rb b/spec/middleware/handle_bad_encoding_middleware_spec.rb
new file mode 100644
index 000000000..8c0d24f18
--- /dev/null
+++ b/spec/middleware/handle_bad_encoding_middleware_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+RSpec.describe HandleBadEncodingMiddleware do
+  let(:app) { double() }
+  let(:middleware) { HandleBadEncodingMiddleware.new(app) }
+
+  it "request with query string is unchanged" do
+    expect(app).to receive(:call).with("PATH" => "/some/path", "QUERY_STRING" => "name=fred")
+    middleware.call("PATH" => "/some/path", "QUERY_STRING" => "name=fred")
+  end
+
+  it "request with no query string is unchanged" do
+    expect(app).to receive(:call).with("PATH" => "/some/path")
+    middleware.call("PATH" => "/some/path")
+  end
+
+  it "request with invalid encoding in query string drops query string" do
+    expect(app).to receive(:call).with("QUERY_STRING" => "", "PATH" => "/some/path")
+    middleware.call("QUERY_STRING" => "q=%2Fsearch%2Fall%Forder%3Ddescending%26page%3D5%26sort%3Dcreated_at", "PATH" => "/some/path")
+  end
+end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index b2f6234cb..3cca9b343 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -823,4 +823,5 @@ RSpec.describe Account, type: :model do
   end
 
   include_examples 'AccountAvatar', :account
+  include_examples 'AccountHeader', :account
 end
diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index 7ddfba7ed..a275621a1 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -133,6 +133,24 @@ RSpec.describe MediaAttachment, type: :model do
       expect(media.file.meta["small"]["height"]).to eq 327
       expect(media.file.meta["small"]["aspect"]).to eq 490.0 / 327
     end
+
+    it 'gives the file a random name' do
+      expect(media.file_file_name).to_not eq 'attachment.jpg'
+    end
+  end
+
+  describe 'base64-encoded jpeg' do
+    let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
+    let(:media) { MediaAttachment.create(account: Fabricate(:account), file: base64_attachment) }
+
+    it 'saves media attachment' do
+      expect(media.persisted?).to be true
+      expect(media.file).to_not be_nil
+    end
+
+    it 'gives the file a file name' do
+      expect(media.file_file_name).to_not be_blank
+    end
   end
 
   describe 'descriptions for remote attachments' do
diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb
index b1abd79b0..c30de8eeb 100644
--- a/spec/services/process_mentions_service_spec.rb
+++ b/spec/services/process_mentions_service_spec.rb
@@ -5,11 +5,11 @@ RSpec.describe ProcessMentionsService, type: :service do
   let(:visibility) { :public }
   let(:status)     { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: visibility) }
 
+  subject { ProcessMentionsService.new }
+
   context 'OStatus with public toot' do
     let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
 
-    subject { ProcessMentionsService.new }
-
     before do
       stub_request(:post, remote_user.salmon_url)
       subject.call(status)
@@ -24,8 +24,6 @@ RSpec.describe ProcessMentionsService, type: :service do
     let(:visibility)  { :private }
     let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
 
-    subject { ProcessMentionsService.new }
-
     before do
       stub_request(:post, remote_user.salmon_url)
       subject.call(status)
@@ -41,29 +39,45 @@ RSpec.describe ProcessMentionsService, type: :service do
   end
 
   context 'ActivityPub' do
-    let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+    context do
+      let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
 
-    subject { ProcessMentionsService.new }
+      before do
+        stub_request(:post, remote_user.inbox_url)
+        subject.call(status)
+      end
 
-    before do
-      stub_request(:post, remote_user.inbox_url)
-      subject.call(status)
-    end
+      it 'creates a mention' do
+        expect(remote_user.mentions.where(status: status).count).to eq 1
+      end
 
-    it 'creates a mention' do
-      expect(remote_user.mentions.where(status: status).count).to eq 1
+      it 'sends activity to the inbox' do
+        expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
+      end
     end
 
-    it 'sends activity to the inbox' do
-      expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
+    context 'with an IDN domain' do
+      let(:remote_user) { Fabricate(:account, username: 'sneak', protocol: :activitypub, domain: 'xn--hresiar-mxa.ch', inbox_url: 'http://example.com/inbox') }
+      let(:status) { Fabricate(:status, account: account, text: "Hello @sneak@hæresiar.ch") }
+
+      before do
+        stub_request(:post, remote_user.inbox_url)
+        subject.call(status)
+      end
+
+      it 'creates a mention' do
+        expect(remote_user.mentions.where(status: status).count).to eq 1
+      end
+
+      it 'sends activity to the inbox' do
+        expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
+      end
     end
   end
 
   context 'Temporarily-unreachable ActivityPub user' do
     let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) }
 
-    subject { ProcessMentionsService.new }
-
     before do
       stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
       stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com").to_return(status: 500)
diff --git a/spec/support/examples/models/concerns/account_avatar.rb b/spec/support/examples/models/concerns/account_avatar.rb
index f2a8a2459..2180f5273 100644
--- a/spec/support/examples/models/concerns/account_avatar.rb
+++ b/spec/support/examples/models/concerns/account_avatar.rb
@@ -16,4 +16,24 @@ shared_examples 'AccountAvatar' do |fabricator|
       end
     end
   end
+
+  describe 'base64-encoded files' do
+    let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
+    let(:account) { Fabricate(fabricator, avatar: base64_attachment) }
+
+    it 'saves avatar' do
+      expect(account.persisted?).to be true
+      expect(account.avatar).to_not be_nil
+    end
+
+    it 'gives the avatar a file name' do
+      expect(account.avatar_file_name).to_not be_blank
+    end
+
+    it 'saves a new avatar under a different file name' do
+      previous_file_name = account.avatar_file_name
+      account.update(avatar: base64_attachment)
+      expect(account.avatar_file_name).to_not eq previous_file_name
+    end
+  end
 end
diff --git a/spec/support/examples/models/concerns/account_header.rb b/spec/support/examples/models/concerns/account_header.rb
new file mode 100644
index 000000000..77ee0e629
--- /dev/null
+++ b/spec/support/examples/models/concerns/account_header.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+shared_examples 'AccountHeader' do |fabricator|
+  describe 'base64-encoded files' do
+    let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
+    let(:account) { Fabricate(fabricator, header: base64_attachment) }
+
+    it 'saves header' do
+      expect(account.persisted?).to be true
+      expect(account.header).to_not be_nil
+    end
+
+    it 'gives the header a file name' do
+      expect(account.header_file_name).to_not be_blank
+    end
+
+    it 'saves a new header under a different file name' do
+      previous_file_name = account.header_file_name
+      account.update(header: base64_attachment)
+      expect(account.header_file_name).to_not eq previous_file_name
+    end
+  end
+end
diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb
new file mode 100644
index 000000000..75b413330
--- /dev/null
+++ b/spec/support/stories/profile_stories.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ProfileStories
+  attr_reader :bob, :alice, :alice_bio
+
+  def as_a_registered_user
+    @bob = Fabricate(
+      :user,
+      email: email, password: password, confirmed_at: confirmed_at,
+      account: Fabricate(:account, username: 'bob')
+    )
+  end
+
+  def as_a_logged_in_user
+    as_a_registered_user
+    visit new_user_session_path
+    fill_in 'user_email', with: email
+    fill_in 'user_password', with: password
+    click_on I18n.t('auth.login')
+  end
+
+  def with_alice_as_local_user
+    @alice_bio = '@alice and @bob are fictional characters commonly used as'\
+                 'placeholder names in #cryptology, as well as #science and'\
+                 'engineering 📖 literature. Not affilated with @pepe.'
+
+    @alice = Fabricate(
+      :user,
+      email: 'alice@example.com', password: password, confirmed_at: confirmed_at,
+      account: Fabricate(:account, username: 'alice', note: @alice_bio)
+    )
+  end
+
+  def confirmed_at
+    @confirmed_at ||= Time.zone.now
+  end
+
+  def email
+    @email ||= 'test@example.com'
+  end
+
+  def password
+    @password ||= 'password'
+  end
+end
diff --git a/spec/workers/refollow_worker_spec.rb b/spec/workers/refollow_worker_spec.rb
new file mode 100644
index 000000000..29771aa59
--- /dev/null
+++ b/spec/workers/refollow_worker_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RefollowWorker do
+  subject { described_class.new }
+  let(:account) { Fabricate(:account, domain: 'example.org', protocol: :activitypub) }
+  let(:alice)   { Fabricate(:account, domain: nil, username: 'alice') }
+  let(:bob)     { Fabricate(:account, domain: nil, username: 'bob') }
+
+  describe 'perform' do
+    let(:service) { double }
+
+    before do
+      allow(FollowService).to receive(:new).and_return(service)
+      allow(service).to receive(:call)
+
+      alice.follow!(account, reblogs: true)
+      bob.follow!(account, reblogs: false)
+    end
+
+    it 'calls FollowService for local followers' do
+      result = subject.perform(account.id)
+
+      expect(result).to be_nil
+      expect(service).to have_received(:call).with(alice, account, reblogs: true)
+      expect(service).to have_received(:call).with(bob, account, reblogs: false)
+    end
+  end
+end