about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/accounts_controller_spec.rb40
-rw-r--r--spec/controllers/activitypub/inboxes_controller_spec.rb7
-rw-r--r--spec/controllers/activitypub/outboxes_controller_spec.rb23
-rw-r--r--spec/controllers/api/oembed_controller_spec.rb1
-rw-r--r--spec/controllers/api/subscriptions_controller_spec.rb26
-rw-r--r--spec/controllers/api/v1/accounts/credentials_controller_spec.rb95
-rw-r--r--spec/controllers/api/v1/accounts/relationships_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/accounts/statuses_controller_spec.rb36
-rw-r--r--spec/controllers/api/v1/accounts_controller_spec.rb69
-rw-r--r--spec/controllers/api/v1/custom_emojis_controller_spec.rb18
-rw-r--r--spec/controllers/api/v1/favourites_controller_spec.rb3
-rw-r--r--spec/controllers/api/v1/follows_controller_spec.rb5
-rw-r--r--spec/controllers/api/v1/media_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/mutes_controller_spec.rb22
-rw-r--r--spec/controllers/api/v1/statuses/favourites_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/statuses/pins_controller_spec.rb57
-rw-r--r--spec/controllers/api/v1/statuses/reblogs_controller_spec.rb2
-rw-r--r--spec/controllers/auth/confirmations_controller_spec.rb18
-rw-r--r--spec/controllers/auth/passwords_controller_spec.rb25
-rw-r--r--spec/controllers/auth/sessions_controller_spec.rb4
-rw-r--r--spec/controllers/concerns/account_controller_concern_spec.rb2
-rw-r--r--spec/controllers/concerns/signature_verification_spec.rb78
-rw-r--r--spec/controllers/home_controller_spec.rb2
-rw-r--r--spec/controllers/remote_follow_controller_spec.rb8
-rw-r--r--spec/controllers/settings/applications_controller_spec.rb188
-rw-r--r--spec/controllers/settings/profiles_controller_spec.rb2
-rw-r--r--spec/controllers/statuses_controller_spec.rb12
-rw-r--r--spec/controllers/stream_entries_controller_spec.rb8
-rw-r--r--spec/fabricators/custom_emoji_fabricator.rb5
-rw-r--r--spec/fabricators/site_upload_fabricator.rb3
-rw-r--r--spec/fabricators/status_fabricator.rb4
-rw-r--r--spec/fabricators/status_pin_fabricator.rb4
-rw-r--r--spec/fixtures/files/emojo.pngbin0 -> 29814 bytes
-rw-r--r--spec/fixtures/requests/activitypub-actor-noinbox.txt9
-rw-r--r--spec/fixtures/requests/activitypub-actor.txt9
-rw-r--r--spec/fixtures/requests/activitypub-feed.txt47
-rw-r--r--spec/fixtures/requests/activitypub-webfinger.txt7
-rw-r--r--spec/helpers/emoji_helper_spec.rb20
-rw-r--r--spec/helpers/jsonld_helper_spec.rb35
-rw-r--r--spec/helpers/routing_helper_spec.rb43
-rw-r--r--spec/helpers/settings_helper_spec.rb4
-rw-r--r--spec/javascript/components/avatar.test.js30
-rw-r--r--spec/javascript/components/avatar_overlay.test.js34
-rw-r--r--spec/javascript/components/display_name.test.js12
-rw-r--r--spec/javascript/components/dropdown_menu.test.js132
-rw-r--r--spec/javascript/components/emojify.test.js16
-rw-r--r--spec/lib/activitypub/activity/accept_spec.rb38
-rw-r--r--spec/lib/activitypub/activity/announce_spec.rb29
-rw-r--r--spec/lib/activitypub/activity/block_spec.rb28
-rw-r--r--spec/lib/activitypub/activity/create_spec.rb350
-rw-r--r--spec/lib/activitypub/activity/delete_spec.rb53
-rw-r--r--spec/lib/activitypub/activity/follow_spec.rb49
-rw-r--r--spec/lib/activitypub/activity/like_spec.rb29
-rw-r--r--spec/lib/activitypub/activity/reject_spec.rb38
-rw-r--r--spec/lib/activitypub/activity/undo_spec.rb107
-rw-r--r--spec/lib/activitypub/activity/update_spec.rb45
-rw-r--r--spec/lib/activitypub/linked_data_signature_spec.rb82
-rw-r--r--spec/lib/activitypub/tag_manager_spec.rb125
-rw-r--r--spec/lib/emoji_spec.rb15
-rw-r--r--spec/lib/formatter_spec.rb130
-rw-r--r--spec/lib/language_detector_spec.rb34
-rw-r--r--spec/lib/ostatus/atom_serializer_spec.rb146
-rw-r--r--spec/lib/ostatus/tag_manager_spec.rb70
-rw-r--r--spec/lib/status_finder_spec.rb (renamed from spec/lib/stream_entry_finder_spec.rb)24
-rw-r--r--spec/lib/tag_manager_spec.rb86
-rw-r--r--spec/models/account_spec.rb1
-rw-r--r--spec/models/concerns/account_interactions_spec.rb40
-rw-r--r--spec/models/custom_emoji_spec.rb25
-rw-r--r--spec/models/import_spec.rb19
-rw-r--r--spec/models/report_spec.rb14
-rw-r--r--spec/models/site_upload_spec.rb5
-rw-r--r--spec/models/status_pin_spec.rb41
-rw-r--r--spec/models/status_spec.rb56
-rw-r--r--spec/models/user_spec.rb20
-rw-r--r--spec/services/activitypub/fetch_remote_account_service_spec.rb123
-rw-r--r--spec/services/activitypub/fetch_remote_status_service_spec.rb75
-rw-r--r--spec/services/activitypub/process_account_service_spec.rb5
-rw-r--r--spec/services/activitypub/process_collection_service_spec.rb55
-rw-r--r--spec/services/authorize_follow_service_spec.rb26
-rw-r--r--spec/services/batched_remove_status_service_spec.rb16
-rw-r--r--spec/services/block_service_spec.rb21
-rw-r--r--spec/services/bootstrap_timeline_service_spec.rb37
-rw-r--r--spec/services/favourite_service_spec.rb24
-rw-r--r--spec/services/fetch_link_card_service_spec.rb19
-rw-r--r--spec/services/fetch_remote_resource_service_spec.rb4
-rw-r--r--spec/services/follow_service_spec.rb29
-rw-r--r--spec/services/mute_service_spec.rb32
-rw-r--r--spec/services/notify_service_spec.rb10
-rw-r--r--spec/services/post_status_service_spec.rb17
-rw-r--r--spec/services/process_feed_service_spec.rb5
-rw-r--r--spec/services/process_mentions_service_spec.rb46
-rw-r--r--spec/services/reblog_service_spec.rb49
-rw-r--r--spec/services/reject_follow_service_spec.rb26
-rw-r--r--spec/services/remove_status_service_spec.rb19
-rw-r--r--spec/services/resolve_remote_account_service_spec.rb97
-rw-r--r--spec/services/unblock_service_spec.rb24
-rw-r--r--spec/services/unfollow_service_spec.rb24
-rw-r--r--spec/services/unsubscribe_service_spec.rb2
-rw-r--r--spec/spec_helper.rb17
-rw-r--r--spec/validators/status_length_validator_spec.rb23
-rw-r--r--spec/views/about/show.html.haml_spec.rb2
-rw-r--r--spec/views/stream_entries/show.html.haml_spec.rb8
-rw-r--r--spec/workers/activitypub/delivery_worker_spec.rb23
-rw-r--r--spec/workers/activitypub/distribution_worker_spec.rb48
-rw-r--r--spec/workers/activitypub/processing_worker_spec.rb15
-rw-r--r--spec/workers/activitypub/update_distribution_worker_spec.rb20
-rw-r--r--spec/workers/pubsubhubbub/distribution_worker_spec.rb31
107 files changed, 3245 insertions, 603 deletions
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index d61c8c9bd..92f888590 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -10,6 +10,13 @@ RSpec.describe AccountsController, type: :controller do
     let!(:status2) { Status.create!(account: alice, text: 'Boop', thread: status1) }
     let!(:status3) { Status.create!(account: alice, text: 'Picture!') }
     let!(:status4) { Status.create!(account: alice, text: 'Mentioning @alice') }
+    let!(:status5) { Status.create!(account: alice, text: 'Kitsune') }
+    let!(:status6) { Status.create!(account: alice, text: 'Neko') }
+    let!(:status7) { Status.create!(account: alice, text: 'Tanuki') }
+
+    let!(:status_pin1) { StatusPin.create!(account: alice, status: status5, created_at: 5.days.ago) }
+    let!(:status_pin2) { StatusPin.create!(account: alice, status: status6, created_at: 2.years.ago) }
+    let!(:status_pin3) { StatusPin.create!(account: alice, status: status7, created_at: 10.minutes.ago) }
 
     before do
       status3.media_attachments.create!(account: alice, file: fixture_file_upload('files/attachment.jpg', 'image/jpeg'))
@@ -48,9 +55,35 @@ RSpec.describe AccountsController, type: :controller do
       it 'returns http success with Activity Streams 2.0' do
         expect(response).to have_http_status(:success)
       end
+
+      it 'returns application/activity+json' do
+        expect(response.content_type).to eq 'application/activity+json'
+      end
     end
 
-    context 'html' do
+    context 'html without since_id nor max_id' do
+      before do
+        get :show, params: { username: alice.username }
+      end
+
+      it 'assigns @account' do
+        expect(assigns(:account)).to eq alice
+      end
+
+      it 'assigns @pinned_statuses' do
+        pinned_statuses = assigns(:pinned_statuses).to_a
+        expect(pinned_statuses.size).to eq 3
+        expect(pinned_statuses[0]).to eq status7
+        expect(pinned_statuses[1]).to eq status5
+        expect(pinned_statuses[2]).to eq status6
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+    end
+
+    context 'html with since_id and max_id' do
       before do
         get :show, params: { username: alice.username, max_id: status4.id, since_id: status1.id }
       end
@@ -66,6 +99,11 @@ RSpec.describe AccountsController, type: :controller do
         expect(statuses[1]).to eq status2
       end
 
+      it 'assigns an empty array to @pinned_statuses' do
+        pinned_statuses = assigns(:pinned_statuses).to_a
+        expect(pinned_statuses.size).to eq 0
+      end
+
       it 'returns http success' do
         expect(response).to have_http_status(:success)
       end
diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb
new file mode 100644
index 000000000..5c12fea7d
--- /dev/null
+++ b/spec/controllers/activitypub/inboxes_controller_spec.rb
@@ -0,0 +1,7 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::InboxesController, type: :controller do
+  describe 'POST #create' do
+    pending
+  end
+end
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
new file mode 100644
index 000000000..a25998021
--- /dev/null
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::OutboxesController, type: :controller do
+  let!(:account) { Fabricate(:account) }
+
+  before do
+    Fabricate(:status, account: account)
+  end
+
+  describe 'GET #show' do
+    before do
+      get :show, params: { account_username: account.username }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'returns application/activity+json' do
+      expect(response.content_type).to eq 'application/activity+json'
+    end
+  end
+end
diff --git a/spec/controllers/api/oembed_controller_spec.rb b/spec/controllers/api/oembed_controller_spec.rb
index 43631a7e5..7af4a6a5b 100644
--- a/spec/controllers/api/oembed_controller_spec.rb
+++ b/spec/controllers/api/oembed_controller_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Api::OEmbedController, type: :controller do
 
   describe 'GET #show' do
     before do
+      request.host = Rails.configuration.x.local_domain
       get :show, params: { url: account_stream_entry_url(alice, status.stream_entry) }, format: :json
     end
 
diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb
index 76f9740ca..d90da9e32 100644
--- a/spec/controllers/api/subscriptions_controller_spec.rb
+++ b/spec/controllers/api/subscriptions_controller_spec.rb
@@ -38,19 +38,19 @@ RSpec.describe Api::SubscriptionsController, type: :controller do
     before do
       stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
       stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
-      stub_request(:head, "https://quitter.no/notice/1269244").to_return(status: 404)
-      stub_request(:head, "https://quitter.no/notice/1265331").to_return(status: 404)
-      stub_request(:head, "https://community.highlandarrow.com/notice/54411").to_return(status: 404)
-      stub_request(:head, "https://community.highlandarrow.com/notice/53857").to_return(status: 404)
-      stub_request(:head, "https://community.highlandarrow.com/notice/51852").to_return(status: 404)
-      stub_request(:head, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404)
-      stub_request(:head, "https://community.highlandarrow.com/notice/50467").to_return(status: 404)
-      stub_request(:head, "https://quitter.no/notice/1243309").to_return(status: 404)
-      stub_request(:head, "https://quitter.no/user/7477").to_return(status: 404)
-      stub_request(:head, "https://community.highlandarrow.com/user/1").to_return(status: 404)
-      stub_request(:head, "https://social.umeahackerspace.se/user/2").to_return(status: 404)
-      stub_request(:head, "https://gs.kawa-kun.com/user/2").to_return(status: 404)
-      stub_request(:head, "https://mastodon.social/users/Gargron").to_return(status: 404)
+      stub_request(:get, "https://quitter.no/notice/1269244").to_return(status: 404)
+      stub_request(:get, "https://quitter.no/notice/1265331").to_return(status: 404)
+      stub_request(:get, "https://community.highlandarrow.com/notice/54411").to_return(status: 404)
+      stub_request(:get, "https://community.highlandarrow.com/notice/53857").to_return(status: 404)
+      stub_request(:get, "https://community.highlandarrow.com/notice/51852").to_return(status: 404)
+      stub_request(:get, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404)
+      stub_request(:get, "https://community.highlandarrow.com/notice/50467").to_return(status: 404)
+      stub_request(:get, "https://quitter.no/notice/1243309").to_return(status: 404)
+      stub_request(:get, "https://quitter.no/user/7477").to_return(status: 404)
+      stub_request(:any, "https://community.highlandarrow.com/user/1").to_return(status: 404)
+      stub_request(:any, "https://social.umeahackerspace.se/user/2").to_return(status: 404)
+      stub_request(:any, "https://gs.kawa-kun.com/user/2").to_return(status: 404)
+      stub_request(:any, "https://mastodon.social/users/Gargron").to_return(status: 404)
 
       request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}"
       request.env['RAW_POST_DATA'] = feed
diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
index 3f655c7b2..247420d08 100644
--- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
@@ -4,52 +4,81 @@ describe Api::V1::Accounts::CredentialsController do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
 
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'GET #show' do
-    it 'returns http success' do
-      get :show
-      expect(response).to have_http_status(:success)
+  context 'with an oauth token' do
+    before do
+      allow(controller).to receive(:doorkeeper_token) { token }
     end
-  end
-
-  describe 'PATCH #update' do
-    describe 'with valid data' do
-      before do
-        patch :update, params: {
-          display_name: "Alice Isn't Dead",
-          note: "Hi!\n\nToot toot!",
-          avatar: fixture_file_upload('files/avatar.gif', 'image/gif'),
-          header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'),
-        }
-      end
 
+    describe 'GET #show' do
       it 'returns http success' do
+        get :show
         expect(response).to have_http_status(:success)
       end
+    end
+
+    describe 'PATCH #update' do
+      describe 'with valid data' do
+        before do
+          allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
+
+          patch :update, params: {
+            display_name: "Alice Isn't Dead",
+            note: "Hi!\n\nToot toot!",
+            avatar: fixture_file_upload('files/avatar.gif', 'image/gif'),
+            header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'),
+          }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(:success)
+        end
 
-      it 'updates account info' do
-        user.account.reload
+        it 'updates account info' do
+          user.account.reload
 
-        expect(user.account.display_name).to eq("Alice Isn't Dead")
-        expect(user.account.note).to eq("Hi!\n\nToot toot!")
-        expect(user.account.avatar).to exist
-        expect(user.account.header).to exist
+          expect(user.account.display_name).to eq("Alice Isn't Dead")
+          expect(user.account.note).to eq("Hi!\n\nToot toot!")
+          expect(user.account.avatar).to exist
+          expect(user.account.header).to exist
+        end
+
+        it 'queues up an account update distribution' do
+          expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(user.account_id)
+        end
+      end
+
+      describe 'with invalid data' do
+        before do
+          note = 'This is too long. '
+          note = note + 'a' * (Account::MAX_NOTE_LENGTH - note.length + 1)
+          patch :update, params: { note: note }
+        end
+
+        it 'returns http unprocessable entity' do
+          expect(response).to have_http_status(:unprocessable_entity)
+        end
       end
     end
+  end
+
+  context 'without an oauth token' do
+    before do
+      allow(controller).to receive(:doorkeeper_token) { nil }
+    end
 
-    describe 'with invalid data' do
-      before do
-        # note length limit is 501, presently hardcoded, so give it 510 to fail
-        patch :update, params: { note: '1234567890' * 51 }
+    describe 'GET #show' do
+      it 'returns http unauthorized' do
+        get :show
+        expect(response).to have_http_status(:unauthorized)
       end
+    end
 
-      it 'returns http unprocessable entity' do
-        expect(response).to have_http_status(:unprocessable_entity)
+    describe 'PATCH #update' do
+      it 'returns http unauthorized' do
+        patch :update, params: { note: 'Foo' }
+        expect(response).to have_http_status(:unauthorized)
       end
     end
   end
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
index 3a9607317..431fc2194 100644
--- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
@@ -50,14 +50,14 @@ describe Api::V1::Accounts::RelationshipsController do
         json = body_as_json
 
         expect(json).to be_a Enumerable
-        expect(json.first[:id]).to be simon.id
+        expect(json.first[:id]).to eq simon.id.to_s
         expect(json.first[:following]).to be true
         expect(json.first[:followed_by]).to be false
         expect(json.first[:muting]).to be false
         expect(json.first[:requested]).to be false
         expect(json.first[:domain_blocking]).to be false
 
-        expect(json.second[:id]).to be lewis.id
+        expect(json.second[:id]).to eq lewis.id.to_s
         expect(json.second[:following]).to be false
         expect(json.second[:followed_by]).to be true
         expect(json.second[:muting]).to be false
diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
index 8b4fd6a5b..c49a77ac3 100644
--- a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
@@ -18,21 +18,37 @@ describe Api::V1::Accounts::StatusesController do
       expect(response).to have_http_status(:success)
       expect(response.headers['Link'].links.size).to eq(2)
     end
-  end
 
-  describe 'GET #index with only media' do
-    it 'returns http success' do
-      get :index, params: { account_id: user.account.id, only_media: true }
+    context 'with only media' do
+      it 'returns http success' do
+        get :index, params: { account_id: user.account.id, only_media: true }
 
-      expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(:success)
+      end
     end
-  end
 
-  describe 'GET #index with exclude replies' do
-    it 'returns http success' do
-      get :index, params: { account_id: user.account.id, exclude_replies: true }
+    context 'with exclude replies' do
+      before do
+        Fabricate(:status, account: user.account, thread: Fabricate(:status))
+      end
 
-      expect(response).to have_http_status(:success)
+      it 'returns http success' do
+        get :index, params: { account_id: user.account.id, exclude_replies: true }
+
+        expect(response).to have_http_status(:success)
+      end
+    end
+
+    context 'with only pinned' do
+      before do
+        Fabricate(:status_pin, account: user.account, status: Fabricate(:status, account: user.account))
+      end
+
+      it 'returns http success' do
+        get :index, params: { account_id: user.account.id, pinned: true }
+
+        expect(response).to have_http_status(:success)
+      end
     end
   end
 end
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index c13509e7b..053c53e5a 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -18,18 +18,48 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
   end
 
   describe 'POST #follow' do
-    let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+    let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account }
 
     before do
       post :follow, params: { id: other_account.id }
     end
 
-    it 'returns http success' do
-      expect(response).to have_http_status(:success)
+    context 'with unlocked account' do
+      let(:locked) { false }
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'returns JSON with following=true and requested=false' do
+        json = body_as_json
+
+        expect(json[:following]).to be true
+        expect(json[:requested]).to be false
+      end
+
+      it 'creates a following relation between user and target user' do
+        expect(user.account.following?(other_account)).to be true
+      end
     end
 
-    it 'creates a following relation between user and target user' do
-      expect(user.account.following?(other_account)).to be true
+    context 'with locked account' do
+      let(:locked) { true }
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'returns JSON with following=false and requested=true' do
+        json = body_as_json
+
+        expect(json[:following]).to be false
+        expect(json[:requested]).to be true
+      end
+
+      it 'creates a follow request relation between user and target user' do
+        expect(user.account.requested?(other_account)).to be true
+      end
     end
   end
 
@@ -107,6 +137,35 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     it 'creates a muting relation' do
       expect(user.account.muting?(other_account)).to be true
     end
+
+    it 'mutes notifications' do
+      expect(user.account.muting_notifications?(other_account)).to be true
+    end
+  end
+
+  describe 'POST #mute with notifications set to false' do
+    let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+
+    before do
+      user.account.follow!(other_account)
+      post :mute, params: {id: other_account.id, notifications: false }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'does not remove the following relation between user and target user' do
+      expect(user.account.following?(other_account)).to be true
+    end
+
+    it 'creates a muting relation' do
+      expect(user.account.muting?(other_account)).to be true
+    end
+
+    it 'does not mute notifications' do
+      expect(user.account.muting_notifications?(other_account)).to be false
+    end
   end
 
   describe 'POST #unmute' do
diff --git a/spec/controllers/api/v1/custom_emojis_controller_spec.rb b/spec/controllers/api/v1/custom_emojis_controller_spec.rb
new file mode 100644
index 000000000..9f3522812
--- /dev/null
+++ b/spec/controllers/api/v1/custom_emojis_controller_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Api::V1::CustomEmojisController, type: :controller do
+  render_views
+
+  describe 'GET #index' do
+    before do
+      Fabricate(:custom_emoji)
+      get :index
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb
index 3de045377..46cf70f4d 100644
--- a/spec/controllers/api/v1/favourites_controller_spec.rb
+++ b/spec/controllers/api/v1/favourites_controller_spec.rb
@@ -70,8 +70,7 @@ RSpec.describe Api::V1::FavouritesController, type: :controller do
         it 'does not add pagination headers if not necessary' do
           get :index
 
-          expect(response.headers['Link'].find_link(['rel', 'next'])).to eq nil
-          expect(response.headers['Link'].find_link(['rel', 'prev'])).to eq nil
+          expect(response.headers['Link']).to eq nil
         end
       end
     end
diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb
index b5e1d16dd..ea9e76d68 100644
--- a/spec/controllers/api/v1/follows_controller_spec.rb
+++ b/spec/controllers/api/v1/follows_controller_spec.rb
@@ -42,5 +42,10 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
     it 'subscribes to remote hub' do
       expect(a_request(:post, "https://quitter.no/main/push/hub")).to have_been_made
     end
+
+    it 'returns http success if already following, too' do
+      post :create, params: { uri: 'gargron@quitter.no' }
+      expect(response).to have_http_status(:success)
+    end
   end
 end
diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb
index 6bad3f05d..baa22d7e4 100644
--- a/spec/controllers/api/v1/media_controller_spec.rb
+++ b/spec/controllers/api/v1/media_controller_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       it 'returns media ID in JSON' do
-        expect(body_as_json[:id]).to eq MediaAttachment.first.id
+        expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
       end
     end
 
@@ -75,7 +75,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       it 'returns media ID in JSON' do
-        expect(body_as_json[:id]).to eq MediaAttachment.first.id
+        expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
       end
     end
 
@@ -97,7 +97,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       xit 'returns media ID in JSON' do
-        expect(body_as_json[:id]).to eq MediaAttachment.first.id
+        expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
       end
     end
   end
diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb
index 3e6fa887b..7387b9d2d 100644
--- a/spec/controllers/api/v1/mutes_controller_spec.rb
+++ b/spec/controllers/api/v1/mutes_controller_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do
   let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
 
   before do
-    Fabricate(:mute, account: user.account)
+    Fabricate(:mute, account: user.account, hide_notifications: false)
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
@@ -18,4 +18,24 @@ RSpec.describe Api::V1::MutesController, type: :controller do
       expect(response).to have_http_status(:success)
     end
   end
+
+  describe 'GET #details' do
+    before do
+      get :details, params: { limit: 1 }
+    end
+
+    let(:mutes) { JSON.parse(response.body) }
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'returns one mute' do
+      expect(mutes.size).to be(1)
+    end
+
+    it 'returns whether the mute hides notifications' do
+      expect(mutes.first["hide_notifications"]).to be(false)
+    end 
+  end
 end
diff --git a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
index 2a029230d..aba7cd458 100644
--- a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
@@ -36,7 +36,7 @@ describe Api::V1::Statuses::FavouritesController do
       it 'return json with updated attributes' do
         hash_body = body_as_json
 
-        expect(hash_body[:id]).to eq status.id
+        expect(hash_body[:id]).to eq status.id.to_s
         expect(hash_body[:favourites_count]).to eq 1
         expect(hash_body[:favourited]).to be true
       end
diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
new file mode 100644
index 000000000..79005c9de
--- /dev/null
+++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Statuses::PinsController do
+  render_views
+
+  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, scopes: 'write', application: app) }
+
+  context 'with an oauth token' do
+    before do
+      allow(controller).to receive(:doorkeeper_token) { token }
+    end
+
+    describe 'POST #create' do
+      let(:status) { Fabricate(:status, account: user.account) }
+
+      before do
+        post :create, params: { status_id: status.id }
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'updates the pinned attribute' do
+        expect(user.account.pinned?(status)).to be true
+      end
+
+      it 'return json with updated attributes' do
+        hash_body = body_as_json
+
+        expect(hash_body[:id]).to eq status.id.to_s
+        expect(hash_body[:pinned]).to be true
+      end
+    end
+
+    describe 'POST #destroy' do
+      let(:status) { Fabricate(:status, account: user.account) }
+
+      before do
+        Fabricate(:status_pin, status: status, account: user.account)
+        post :destroy, params: { status_id: status.id }
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'updates the pinned attribute' do
+        expect(user.account.pinned?(status)).to be false
+      end
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
index d6d36c1b2..7417ff672 100644
--- a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
@@ -36,7 +36,7 @@ describe Api::V1::Statuses::ReblogsController do
       it 'return json with updated attributes' do
         hash_body = body_as_json
 
-        expect(hash_body[:reblog][:id]).to eq status.id
+        expect(hash_body[:reblog][:id]).to eq status.id.to_s
         expect(hash_body[:reblog][:reblogs_count]).to eq 1
         expect(hash_body[:reblog][:reblogged]).to be true
       end
diff --git a/spec/controllers/auth/confirmations_controller_spec.rb b/spec/controllers/auth/confirmations_controller_spec.rb
index cf7f91e52..2ec36c060 100644
--- a/spec/controllers/auth/confirmations_controller_spec.rb
+++ b/spec/controllers/auth/confirmations_controller_spec.rb
@@ -10,4 +10,22 @@ describe Auth::ConfirmationsController, type: :controller do
       expect(response).to have_http_status(:success)
     end
   end
+
+  describe 'GET #show' do
+    let!(:user) { Fabricate(:user, confirmation_token: 'foobar', confirmed_at: nil) }
+
+    before do
+      allow(BootstrapTimelineWorker).to receive(:perform_async)
+      @request.env['devise.mapping'] = Devise.mappings[:user]
+      get :show, params: { confirmation_token: 'foobar' }
+    end
+
+    it 'redirects to login' do
+      expect(response).to redirect_to(new_user_session_path)
+    end
+
+    it 'queues up bootstrapping of home timeline' do
+      expect(BootstrapTimelineWorker).to have_received(:perform_async).with(user.account_id)
+    end
+  end
 end
diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb
index 60b225efa..992d2e29d 100644
--- a/spec/controllers/auth/passwords_controller_spec.rb
+++ b/spec/controllers/auth/passwords_controller_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 describe Auth::PasswordsController, type: :controller do
+  include Devise::Test::ControllerHelpers
+
   describe 'GET #new' do
     it 'returns http success' do
       @request.env['devise.mapping'] = Devise.mappings[:user]
@@ -10,4 +12,27 @@ describe Auth::PasswordsController, type: :controller do
       expect(response).to have_http_status(:success)
     end
   end
+
+  describe 'GET #edit' do
+    let(:user) { Fabricate(:user) }
+
+    before do
+      request.env['devise.mapping'] = Devise.mappings[:user]
+      @token = user.send_reset_password_instructions
+    end
+
+    context 'with valid reset_password_token' do
+      it 'returns http success' do
+        get :edit, params: { reset_password_token: @token }
+        expect(response).to have_http_status(:success)
+      end
+    end
+
+    context 'with invalid reset_password_token' do
+      it 'redirects to #new' do
+        get :edit, params: { reset_password_token: 'some_invalid_value' }
+        expect(response).to redirect_to subject.new_password_path(subject.send(:resource_name))
+      end
+    end
+  end
 end
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index 06fdbaabc..88f0a4734 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
         sign_in(user, scope: :user)
         delete :destroy
 
-        expect(response).to redirect_to(root_path)
+        expect(response).to redirect_to(new_user_session_path)
       end
     end
 
@@ -38,7 +38,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
         sign_in(user, scope: :user)
         delete :destroy
 
-        expect(response).to redirect_to(root_path)
+        expect(response).to redirect_to(new_user_session_path)
       end
     end
   end
diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb
index bdc181edc..ae46f9ba6 100644
--- a/spec/controllers/concerns/account_controller_concern_spec.rb
+++ b/spec/controllers/concerns/account_controller_concern_spec.rb
@@ -33,7 +33,7 @@ describe ApplicationController, type: :controller do
     it 'sets link headers' do
       account = Fabricate(:account, username: 'username')
       get 'success', params: { account_username: 'username' }
-      expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/xrd+xml", <http://test.host/users/username.atom>; rel="alternate"; type="application/atom+xml"'
+      expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/xrd+xml", <http://test.host/users/username.atom>; rel="alternate"; type="application/atom+xml", <https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"'
     end
 
     it 'returns http success' do
diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb
index b371795ab..64648621e 100644
--- a/spec/controllers/concerns/signature_verification_spec.rb
+++ b/spec/controllers/concerns/signature_verification_spec.rb
@@ -16,7 +16,7 @@ describe ApplicationController, type: :controller do
   end
 
   before do
-    routes.draw { get 'success' => 'anonymous#success' }
+    routes.draw { match via: [:get, :post], 'success' => 'anonymous#success' }
   end
 
   context 'without signature header' do
@@ -40,34 +40,74 @@ describe ApplicationController, type: :controller do
   context 'with signature header' do
     let!(:author) { Fabricate(:account) }
 
-    before do
-      get :success
+    context 'without body' do
+      before do
+        get :success
 
-      fake_request = Request.new(:get, request.url)
-      fake_request.on_behalf_of(author)
+        fake_request = Request.new(:get, request.url)
+        fake_request.on_behalf_of(author)
 
-      request.headers.merge!(fake_request.headers)
-    end
+        request.headers.merge!(fake_request.headers)
+      end
 
-    describe '#signed_request?' do
-      it 'returns true' do
-        expect(controller.signed_request?).to be true
+      describe '#signed_request?' do
+        it 'returns true' do
+          expect(controller.signed_request?).to be true
+        end
+      end
+
+      describe '#signed_request_account' do
+        it 'returns an account' do
+          expect(controller.signed_request_account).to eq author
+        end
+
+        it 'returns nil when path does not match' do
+          request.path = '/alternative-path'
+          expect(controller.signed_request_account).to be_nil
+        end
+
+        it 'returns nil when method does not match' do
+          post :success
+          expect(controller.signed_request_account).to be_nil
+        end
       end
     end
 
-    describe '#signed_request_account' do
-      it 'returns an account' do
-        expect(controller.signed_request_account).to eq author
+    context 'with body' do
+      before do
+        post :success, body: 'Hello world'
+
+        fake_request = Request.new(:post, request.url, body: 'Hello world')
+        fake_request.on_behalf_of(author)
+
+        request.headers.merge!(fake_request.headers)
       end
 
-      it 'returns nil when path does not match' do
-        request.path = '/alternative-path'
-        expect(controller.signed_request_account).to be_nil
+      describe '#signed_request?' do
+        it 'returns true' do
+          expect(controller.signed_request?).to be true
+        end
       end
 
-      it 'returns nil when method does not match' do
-        post :success
-        expect(controller.signed_request_account).to be_nil
+      describe '#signed_request_account' do
+        it 'returns an account' do
+          expect(controller.signed_request_account).to eq author
+        end
+
+        it 'returns nil when path does not match' do
+          request.path = '/alternative-path'
+          expect(controller.signed_request_account).to be_nil
+        end
+
+        it 'returns nil when method does not match' do
+          get :success
+          expect(controller.signed_request_account).to be_nil
+        end
+
+        it 'returns nil when body has been tampered' do
+          request.headers['RAW_POST_DATA'] = 'doo doo doo'
+          expect(controller.signed_request_account).to be_nil
+        end
       end
     end
   end
diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb
index d44d720b1..1077a7288 100644
--- a/spec/controllers/home_controller_spec.rb
+++ b/spec/controllers/home_controller_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe HomeController, type: :controller do
   describe 'GET #index' do
     context 'when not signed in' do
       it 'redirects to about page' do
+        @request.path = '/'
         get :index
         expect(response).to redirect_to(about_path)
       end
@@ -13,6 +14,7 @@ RSpec.describe HomeController, type: :controller do
 
     context 'when signed in' do
       let(:user) { Fabricate(:user) }
+
       subject do
         sign_in(user)
         get :index
diff --git a/spec/controllers/remote_follow_controller_spec.rb b/spec/controllers/remote_follow_controller_spec.rb
index 915c86f8e..86b1eb8d0 100644
--- a/spec/controllers/remote_follow_controller_spec.rb
+++ b/spec/controllers/remote_follow_controller_spec.rb
@@ -87,6 +87,14 @@ describe RemoteFollowController do
         expect(response).to render_template(:new)
         expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
       end
+
+      it 'renders new when occur HTTP::ConnectionError' do
+        allow(Goldfinger).to receive(:finger).with('acct:user@unknown').and_raise(HTTP::ConnectionError)
+        post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@unknown' } }
+
+        expect(response).to render_template(:new)
+        expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
+      end
     end
   end
 
diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb
new file mode 100644
index 000000000..ca66f8d23
--- /dev/null
+++ b/spec/controllers/settings/applications_controller_spec.rb
@@ -0,0 +1,188 @@
+require 'rails_helper'
+
+describe Settings::ApplicationsController do
+  render_views
+  
+  let!(:user) { Fabricate(:user) }
+  let!(:app) { Fabricate(:application, owner: user) }
+  
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #index' do
+    let!(:other_app) { Fabricate(:application) }
+
+    it 'shows apps' do
+      get :index
+      expect(response).to have_http_status(:success)
+      expect(assigns(:applications)).to include(app)
+      expect(assigns(:applications)).to_not include(other_app)
+    end
+  end
+
+  
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show, params: { id: app.id }
+      expect(response).to have_http_status(:success)
+      expect(assigns[:application]).to eql(app)
+    end
+
+    it 'returns 404 if you dont own app' do
+      app.update!(owner: nil)
+
+      get :show, params: { id: app.id }
+      expect(response.status).to eq 404
+    end
+  end
+
+  describe 'GET #new' do
+    it 'works' do
+      get :new
+      expect(response).to have_http_status(:success)
+    end
+  end
+
+  describe 'POST #create' do
+    context 'success (passed scopes as a String)' do
+      def call_create
+        post :create, params: {
+               doorkeeper_application: {
+                 name: 'My New App',
+                 redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
+                 website: 'http://google.com',
+                 scopes: 'read write follow'
+               }
+             }
+        response
+      end
+
+      it 'creates an entry in the database' do
+        expect { call_create }.to change(Doorkeeper::Application, :count)
+      end
+
+      it 'redirects back to applications page' do
+        expect(call_create).to redirect_to(settings_applications_path)
+      end
+    end
+
+    context 'success (passed scopes as an Array)' do
+      def call_create
+        post :create, params: {
+               doorkeeper_application: {
+                 name: 'My New App',
+                 redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
+                 website: 'http://google.com',
+                 scopes: [ 'read', 'write', 'follow' ]
+               }
+             }
+        response
+      end
+
+      it 'creates an entry in the database' do
+        expect { call_create }.to change(Doorkeeper::Application, :count)
+      end
+
+      it 'redirects back to applications page' do
+        expect(call_create).to redirect_to(settings_applications_path)
+      end
+    end
+
+    context 'failure' do
+      before do
+        post :create, params: {
+               doorkeeper_application: {
+                 name: '',
+                 redirect_uri: '',
+                 website: '',
+                 scopes: []
+               }
+             }
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'renders form again' do
+        expect(response).to render_template(:new)
+      end
+    end
+  end
+  
+  describe 'PATCH #update' do
+    context 'success' do
+      let(:opts) {
+        {
+          website: 'https://foo.bar/'
+        }
+      }
+
+      def call_update
+        patch :update, params: {
+                id: app.id,
+                doorkeeper_application: opts
+              }
+        response
+      end
+
+      it 'updates existing application' do
+        call_update
+        expect(app.reload.website).to eql(opts[:website])
+      end
+      
+      it 'redirects back to applications page' do
+        expect(call_update).to redirect_to(settings_applications_path)
+      end
+    end
+
+    context 'failure' do
+      before do
+        patch :update, params: {
+                id: app.id,
+                doorkeeper_application: {
+                  name: '',
+                  redirect_uri: '',
+                  website: '',
+                  scopes: []
+                }
+              }
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'renders form again' do
+        expect(response).to render_template(:show)
+      end
+    end
+  end
+
+  describe 'destroy' do
+    before do
+      post :destroy, params: { id: app.id }
+    end
+
+    it 'redirects back to applications page' do
+      expect(response).to redirect_to(settings_applications_path)
+    end
+
+    it 'removes the app' do
+      expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil
+    end
+  end
+
+  describe 'regenerate' do
+    let(:token) { user.token_for_app(app) }
+    before do
+      expect(token).to_not be_nil
+      post :regenerate, params: { id: app.id }
+    end
+
+    it 'should create new token' do
+      expect(user.token_for_app(app)).to_not eql(token)
+    end
+  end
+end
diff --git a/spec/controllers/settings/profiles_controller_spec.rb b/spec/controllers/settings/profiles_controller_spec.rb
index e502dbda7..ee3315be6 100644
--- a/spec/controllers/settings/profiles_controller_spec.rb
+++ b/spec/controllers/settings/profiles_controller_spec.rb
@@ -17,11 +17,13 @@ RSpec.describe Settings::ProfilesController, type: :controller do
 
   describe 'PUT #update' do
     it 'updates the user profile' do
+      allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
       account = Fabricate(:account, user: @user, display_name: 'Old name')
 
       put :update, params: { account: { display_name: 'New name' } }
       expect(account.reload.display_name).to eq 'New name'
       expect(response).to redirect_to(settings_profile_path)
+      expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
     end
   end
 end
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 88d365624..95fb4d594 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -30,6 +30,18 @@ describe StatusesController do
       end
     end
 
+    context 'status is a reblog' do
+      it 'redirects to the original status' do
+        original_account = Fabricate(:account, domain: 'example.com')
+        original_status = Fabricate(:status, account: original_account, uri: 'tag:example.com,2017:foo', url: 'https://example.com/123')
+        status = Fabricate(:status, reblog: original_status)
+
+        get :show, params: { account_username: status.account.username, id: status.id }
+
+        expect(response).to redirect_to(original_status.url)
+      end
+    end
+
     context 'account is not suspended and status is permitted' do
       it 'assigns @account' do
         status = Fabricate(:status)
diff --git a/spec/controllers/stream_entries_controller_spec.rb b/spec/controllers/stream_entries_controller_spec.rb
index 2cc428e0c..f81e2be7b 100644
--- a/spec/controllers/stream_entries_controller_spec.rb
+++ b/spec/controllers/stream_entries_controller_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe StreamEntriesController, type: :controller do
 
         get route, params: { account_username: alice.username, id: status.stream_entry.id }
 
-        expect(response.headers['Link'].to_s).to eq "<http://test.host/users/alice/updates/#{status.stream_entry.id}.atom>; rel=\"alternate\"; type=\"application/atom+xml\""
+        expect(response.headers['Link'].to_s).to eq "<http://test.host/users/alice/updates/#{status.stream_entry.id}.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://cb6e6126.ngrok.io/users/alice/statuses/#{status.id}>; rel=\"alternate\"; type=\"application/activity+json\""
       end
     end
 
@@ -88,14 +88,12 @@ RSpec.describe StreamEntriesController, type: :controller do
   describe 'GET #embed' do
     include_examples 'before_action', :embed
 
-    it 'returns embedded view of status' do
+    it 'redirects to new embed page' do
       status = Fabricate(:status)
 
       get :embed, params: { account_username: status.account.username, id: status.stream_entry.id }
 
-      expect(response).to have_http_status(:success)
-      expect(response.headers['X-Frame-Options']).to eq 'ALLOWALL'
-      expect(response).to render_template(layout: 'embedded')
+      expect(response).to redirect_to(embed_short_account_status_url(status.account, status))
     end
   end
 end
diff --git a/spec/fabricators/custom_emoji_fabricator.rb b/spec/fabricators/custom_emoji_fabricator.rb
new file mode 100644
index 000000000..18a7d23dc
--- /dev/null
+++ b/spec/fabricators/custom_emoji_fabricator.rb
@@ -0,0 +1,5 @@
+Fabricator(:custom_emoji) do
+  shortcode 'coolcat'
+  domain    nil
+  image     { File.open(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png')) }
+end
diff --git a/spec/fabricators/site_upload_fabricator.rb b/spec/fabricators/site_upload_fabricator.rb
new file mode 100644
index 000000000..8f4e43ac9
--- /dev/null
+++ b/spec/fabricators/site_upload_fabricator.rb
@@ -0,0 +1,3 @@
+Fabricator(:site_upload) do
+
+end
diff --git a/spec/fabricators/status_fabricator.rb b/spec/fabricators/status_fabricator.rb
index 8ec5f4ba7..04bbbcf4b 100644
--- a/spec/fabricators/status_fabricator.rb
+++ b/spec/fabricators/status_fabricator.rb
@@ -1,4 +1,8 @@
 Fabricator(:status) do
   account
   text "Lorem ipsum dolor sit amet"
+
+  after_build do |status|
+    status.uri = Faker::Internet.device_token if !status.account.local? && status.uri.nil?
+  end
 end
diff --git a/spec/fabricators/status_pin_fabricator.rb b/spec/fabricators/status_pin_fabricator.rb
new file mode 100644
index 000000000..6a9006c9f
--- /dev/null
+++ b/spec/fabricators/status_pin_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:status_pin) do
+  account
+  status
+end
diff --git a/spec/fixtures/files/emojo.png b/spec/fixtures/files/emojo.png
new file mode 100644
index 000000000..cb5993499
--- /dev/null
+++ b/spec/fixtures/files/emojo.png
Binary files differdiff --git a/spec/fixtures/requests/activitypub-actor-noinbox.txt b/spec/fixtures/requests/activitypub-actor-noinbox.txt
new file mode 100644
index 000000000..95b4650e0
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-actor-noinbox.txt
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Date: Sun, 17 Sep 2017 06:51:23 GMT
+Content-Type: application/json; charset=utf-8
+X-XSS-Protection: 1; mode=block
+Link: <https://ap.example.com/.well-known/webfinger?resource=acct%3Afoo%40ap.example.com>; rel="lrdd"; type="application/xrd+xml", <https://ap.example.com/users/foo.atom>; rel="alternate"; type="application/atom+xml"
+Vary: Accept-Encoding
+Cache-Control: max-age=0, private, must-revalidate
+
+{"@context":"https://www.w3.org/ns/activitystreams","id":"https://ap.example.com/users/foo","type":"Person","following":"https://ap.example.com/users/foo/following","followers":"https://ap.example.com/users/foo/followers","inbox":null,"outbox":"https://ap.example.com/users/foo/outbox","preferredUsername":"foo","name":"","summary":"\u003cp\u003etest\u003c/p\u003e","icon":"https://quitter.no/avatar/7477-300-20160211190340.png","image":"/headers/original/missing.png","publicKey":{"id":"https://ap.example.com/users/foo#main-key","owner":"https://ap.example.com/users/foo","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3L4vnpNLzVH31MeWI39\n4F0wKeJFsLDAsNXGeOu0QF2x+h1zLWZw/agqD2R3JPU9/kaDJGPIV2Sn5zLyUA9S\n6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh\n8lDET6X4Pyw+ZJU0/OLo/41q9w+OrGtlsTm/PuPIeXnxa6BLqnDaxC+4IcjG/FiP\nahNCTINl/1F/TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq+t8nhQYkgAkt64euW\npva3qL5KD1mTIZQEP+LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3Qvu\nHQIDAQAB\n-----END PUBLIC KEY-----\n"}}
\ No newline at end of file
diff --git a/spec/fixtures/requests/activitypub-actor.txt b/spec/fixtures/requests/activitypub-actor.txt
new file mode 100644
index 000000000..6514241cb
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-actor.txt
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/activity+json; charset=utf-8
+Link: <https://ap.example.com/.well-known/webfinger?resource=acct%3Afoo%40ap.example.com>; rel="lrdd"; type="application/xrd+xml", <https://ap.example.com/users/foo.atom>; rel="alternate"; type="application/atom+xml", <https://ap.example.com/users/foo>; rel="alternate"; type="application/activity+json"
+Vary: Accept-Encoding
+X-Content-Type-Options: nosniff
+X-Xss-Protection: 1; mode=block
+
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation"}],"id":"https://ap.example.com/users/foo","type":"Person","following":"https://ap.example.com/users/foo/following","followers":"https://ap.example.com/users/foo/followers","inbox":"https://ap.example.com/users/foo/inbox","outbox":"https://ap.example.com/users/foo/outbox","preferredUsername":"foo","name":"","summary":"\u003cp\u003etest\u003c/p\u003e","url":"https://ap.example.com/@foo","manuallyApprovesFollowers":false,"publicKey":{"id":"https://ap.example.com/users/foo#main-key","owner":"https://ap.example.com/users/foo","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3L4vnpNLzVH31MeWI39\n4F0wKeJFsLDAsNXGeOu0QF2x+h1zLWZw/agqD2R3JPU9/kaDJGPIV2Sn5zLyUA9S\n6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh\n8lDET6X4Pyw+ZJU0/OLo/41q9w+OrGtlsTm/PuPIeXnxa6BLqnDaxC+4IcjG/FiP\nahNCTINl/1F/TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq+t8nhQYkgAkt64euW\npva3qL5KD1mTIZQEP+LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3Qvu\nHQIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"https://ap.example.com/inbox"},"icon":{"type":"Image","url":"https://quitter.no/avatar/7477-300-20160211190340.png"}}
\ No newline at end of file
diff --git a/spec/fixtures/requests/activitypub-feed.txt b/spec/fixtures/requests/activitypub-feed.txt
new file mode 100644
index 000000000..84fd414c3
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-feed.txt
@@ -0,0 +1,47 @@
+HTTP/1.1 200 OK
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/atom+xml; charset=utf-8
+Link: <https://ap.example.com/.well-known/webfinger?resource=acct%3Afoo%40ap.example.com>; rel="lrdd"; type="application/xrd+xml", <https://ap.example.com/users/foo.atom>; rel="alternate"; type="application/atom+xml", <https://ap.example.com/users/foo>; rel="alternate"; type="application/activity+json"
+Vary: Accept-Encoding
+Date: Sun, 17 Sep 2017 06:33:53 GMT
+
+<?xml version="1.0"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
+  <id>https://ap.example.com/users/foo.atom</id>
+  <title>foo</title>
+  <subtitle>test</subtitle>
+  <updated>2017-09-16T18:50:09Z</updated>
+  <logo>https://ap.example.com/system/accounts/avatars/000/000/001/original/141ee5846d159cba.png?1505587809</logo>
+  <author>
+    <id>https://ap.example.com/users/foo</id>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+    <uri>https://ap.example.com/users/foo</uri>
+    <name>foo</name>
+    <email>foo@ap.example.com</email>
+    <summary type="html">&lt;p&gt;test&lt;/p&gt;</summary>
+    <link rel="alternate" type="text/html" href="https://ap.example.com/@foo"/>
+    <link rel="avatar" type="image/jpeg" media:width="120" media:height="120" href="https://quitter.no/avatar/7477-300-20160211190340.png"/>
+    <poco:preferredUsername>foo</poco:preferredUsername>
+    <poco:note>test</poco:note>
+    <mastodon:scope>public</mastodon:scope>
+  </author>
+  <link rel="alternate" type="text/html" href="https://ap.example.com/@foo"/>
+  <link rel="self" type="application/atom+xml" href="https://ap.example.com/users/foo.atom"/>
+  <link rel="hub" href="https://ap.example.com/api/push"/>
+  <link rel="salmon" href="https://ap.example.com/api/salmon/1"/>
+  <entry>
+    <id>https://ap.example.com/users/foo/statuses/11076</id>
+    <published>2017-09-13T01:23:19Z</published>
+    <updated>2017-09-13T01:23:19Z</updated>
+    <title>New status by foo</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <link rel="alternate" type="application/activity+json" href="https://ap.example.com/users/foo/statuses/11076"/>
+    <content type="html" xml:lang="ja">&lt;p&gt;test&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://ap.example.com/@foo/11076"/>
+    <link rel="self" type="application/atom+xml" href="https://ap.example.com/users/foo/updates/11015.atom"/>
+    <ostatus:conversation ref="tag:ap.example.com,2017-09-13:objectId=7412:objectType=Conversation"/>
+  </entry>
+</feed>
diff --git a/spec/fixtures/requests/activitypub-webfinger.txt b/spec/fixtures/requests/activitypub-webfinger.txt
new file mode 100644
index 000000000..465066d84
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-webfinger.txt
@@ -0,0 +1,7 @@
+HTTP/1.1 200 OK
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/jrd+json; charset=utf-8
+X-Content-Type-Options: nosniff
+Date: Sun, 17 Sep 2017 06:22:50 GMT
+
+{"subject":"acct:foo@ap.example.com","aliases":["https://ap.example.com/@foo","https://ap.example.com/users/foo"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://ap.example.com/@foo"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://ap.example.com/users/foo.atom"},{"rel":"self","type":"application/activity+json","href":"https://ap.example.com/users/foo"},{"rel":"salmon","href":"https://ap.example.com/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.u3L4vnpNLzVH31MeWI394F0wKeJFsLDAsNXGeOu0QF2x-h1zLWZw_agqD2R3JPU9_kaDJGPIV2Sn5zLyUA9S6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh8lDET6X4Pyw-ZJU0_OLo_41q9w-OrGtlsTm_PuPIeXnxa6BLqnDaxC-4IcjG_FiPahNCTINl_1F_TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq-t8nhQYkgAkt64euWpva3qL5KD1mTIZQEP-LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3QvuHQ==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://ap.example.com/authorize_follow?acct={uri}"}]}
\ No newline at end of file
diff --git a/spec/helpers/emoji_helper_spec.rb b/spec/helpers/emoji_helper_spec.rb
deleted file mode 100644
index 6edf7672f..000000000
--- a/spec/helpers/emoji_helper_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe EmojiHelper, type: :helper do
-  describe '#emojify' do
-    it 'converts shortcodes to unicode' do
-      text = ':book: Book'
-      expect(emojify(text)).to eq '📖 Book'
-    end
-
-    it 'converts composite emoji shortcodes to unicode' do
-      text = ':couple_ww:'
-      expect(emojify(text)).to eq '👩❤👩'
-    end
-
-    it 'does not convert shortcodes that are part of a string into unicode' do
-      text = ':see_no_evil::hear_no_evil::speak_no_evil:'
-      expect(emojify(text)).to eq text
-    end
-  end
-end
diff --git a/spec/helpers/jsonld_helper_spec.rb b/spec/helpers/jsonld_helper_spec.rb
new file mode 100644
index 000000000..7d3912e6c
--- /dev/null
+++ b/spec/helpers/jsonld_helper_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe JsonLdHelper do
+  describe '#equals_or_includes?' do
+    it 'returns true when value equals' do
+      expect(helper.equals_or_includes?('foo', 'foo')).to be true
+    end
+
+    it 'returns false when value does not equal' do
+      expect(helper.equals_or_includes?('foo', 'bar')).to be false
+    end
+
+    it 'returns true when value is included' do
+      expect(helper.equals_or_includes?(%w(foo baz), 'foo')).to be true
+    end
+
+    it 'returns false when value is not included' do
+      expect(helper.equals_or_includes?(%w(foo baz), 'bar')).to be false
+    end
+  end
+
+  describe '#first_of_value' do
+    pending
+  end
+
+  describe '#supported_context?' do
+    pending
+  end
+
+  describe '#fetch_resource' do
+    pending
+  end
+end
diff --git a/spec/helpers/routing_helper_spec.rb b/spec/helpers/routing_helper_spec.rb
new file mode 100644
index 000000000..940392c9b
--- /dev/null
+++ b/spec/helpers/routing_helper_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe RoutingHelper, type: :helper do
+  describe '.full_asset_url' do
+    around do |example|
+      use_s3 = Rails.configuration.x.use_s3
+      example.run
+      Rails.configuration.x.use_s3 = use_s3
+    end
+
+    shared_examples 'returns full path URL' do
+      it 'with host' do
+        url = helper.full_asset_url('https://example.com/avatars/000/000/002/original/icon.png')
+
+        expect(url).to eq 'https://example.com/avatars/000/000/002/original/icon.png'
+      end
+
+      it 'without host' do
+        url = helper.full_asset_url('/avatars/original/missing.png', skip_pipeline: true)
+
+        expect(url).to eq 'http://test.host/avatars/original/missing.png'
+      end
+    end
+
+    context 'Do not use S3' do
+      before do
+        Rails.configuration.x.use_s3 = false
+      end
+
+      it_behaves_like 'returns full path URL'
+    end
+
+    context 'Use S3' do
+      before do
+        Rails.configuration.x.use_s3 = true
+      end
+
+      it_behaves_like 'returns full path URL'
+    end
+  end
+end
diff --git a/spec/helpers/settings_helper_spec.rb b/spec/helpers/settings_helper_spec.rb
index 5a51e0ef1..092c37583 100644
--- a/spec/helpers/settings_helper_spec.rb
+++ b/spec/helpers/settings_helper_spec.rb
@@ -4,10 +4,10 @@ require 'rails_helper'
 
 describe SettingsHelper do
   describe 'the HUMAN_LOCALES constant' do
-    it 'has the same number of keys as I18n locales exist' do
+    it 'includes all I18n locales' do
       options = I18n.available_locales
 
-      expect(described_class::HUMAN_LOCALES.keys).to eq(options)
+      expect(described_class::HUMAN_LOCALES.keys).to include(*options)
     end
   end
 
diff --git a/spec/javascript/components/avatar.test.js b/spec/javascript/components/avatar.test.js
index 03b71dc9d..ee40812ca 100644
--- a/spec/javascript/components/avatar.test.js
+++ b/spec/javascript/components/avatar.test.js
@@ -1,20 +1,42 @@
 import { expect } from 'chai';
 import { render } from 'enzyme';
+import { fromJS }  from 'immutable';
 import React from 'react';
 import Avatar from '../../../app/javascript/mastodon/components/avatar';
 
 describe('<Avatar />', () => {
-  const src = '/path/to/image.jpg';
+  const account = fromJS({
+    username: 'alice',
+    acct: 'alice',
+    display_name: 'Alice',
+    avatar: '/animated/alice.gif',
+    avatar_static: '/static/alice.jpg',
+  });
   const size = 100;
-  const wrapper = render(<Avatar src={src} animate size={size} />);
+  const animated = render(<Avatar account={account} animate size={size} />);
+  const still = render(<Avatar account={account} size={size} />);
 
+  // Autoplay
   it('renders a div element with the given src as background', () => {
-    expect(wrapper.find('div')).to.have.style('background-image', `url(${src})`);
+    expect(animated.find('div')).to.have.style('background-image', `url(${account.get('avatar')})`);
   });
 
   it('renders a div element of the given size', () => {
     ['width', 'height'].map((attr) => {
-      expect(wrapper.find('div')).to.have.style(attr, `${size}px`);
+      expect(animated.find('div')).to.have.style(attr, `${size}px`);
+    });
+  });
+
+  // Still
+  it('renders a div element with the given static src as background if not autoplay', () => {
+    expect(still.find('div')).to.have.style('background-image', `url(${account.get('avatar_static')})`);
+  });
+
+  it('renders a div element of the given size if not autoplay', () => {
+    ['width', 'height'].map((attr) => {
+      expect(still.find('div')).to.have.style(attr, `${size}px`);
     });
   });
+
+  // TODO add autoplay test if possible
 });
diff --git a/spec/javascript/components/avatar_overlay.test.js b/spec/javascript/components/avatar_overlay.test.js
new file mode 100644
index 000000000..a8f0e13d5
--- /dev/null
+++ b/spec/javascript/components/avatar_overlay.test.js
@@ -0,0 +1,34 @@
+import { expect } from 'chai';
+import { render } from 'enzyme';
+import { fromJS }  from 'immutable';
+import React from 'react';
+import AvatarOverlay from '../../../app/javascript/mastodon/components/avatar_overlay';
+
+describe('<Avatar />', () => {
+  const account = fromJS({
+    username: 'alice',
+    acct: 'alice',
+    display_name: 'Alice',
+    avatar: '/animated/alice.gif',
+    avatar_static: '/static/alice.jpg',
+  });
+  const friend = fromJS({
+    username: 'eve',
+    acct: 'eve@blackhat.lair',
+    display_name: 'Evelyn',
+    avatar: '/animated/eve.gif',
+    avatar_static: '/static/eve.jpg',
+  });
+
+  const overlay = render(<AvatarOverlay account={account} friend={friend} />);
+
+  it('renders account static src as base of overlay avatar', () => {
+    expect(overlay.find('.account__avatar-overlay-base'))
+      .to.have.style('background-image', `url(${account.get('avatar_static')})`);
+  });
+
+  it('renders friend static src as overlay of overlay avatar', () => {
+    expect(overlay.find('.account__avatar-overlay-overlay'))
+      .to.have.style('background-image', `url(${friend.get('avatar_static')})`);
+  });
+});
diff --git a/spec/javascript/components/display_name.test.js b/spec/javascript/components/display_name.test.js
index ad9288d4d..ab484cf3e 100644
--- a/spec/javascript/components/display_name.test.js
+++ b/spec/javascript/components/display_name.test.js
@@ -9,19 +9,9 @@ describe('<DisplayName />', () => {
     const account = fromJS({
       username: 'bar',
       acct: 'bar@baz',
-      display_name: 'Foo',
+      display_name_html: '<p>Foo</p>',
     });
     const wrapper = render(<DisplayName account={account} />);
     expect(wrapper).to.have.text('Foo @bar@baz');
   });
-
-  it('renders the username + account name if display name is empty', () => {
-    const account = fromJS({
-      username: 'bar',
-      acct: 'bar@baz',
-      display_name: '',
-    });
-    const wrapper = render(<DisplayName account={account} />);
-    expect(wrapper).to.have.text('bar @bar@baz');
-  });
 });
diff --git a/spec/javascript/components/dropdown_menu.test.js b/spec/javascript/components/dropdown_menu.test.js
deleted file mode 100644
index a5af730ef..000000000
--- a/spec/javascript/components/dropdown_menu.test.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import { expect } from 'chai';
-import { shallow, mount } from 'enzyme';
-import sinon from 'sinon';
-import React from 'react';
-import DropdownMenu from '../../../app/javascript/mastodon/components/dropdown_menu';
-import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
-
-const isTrue = () => true;
-
-describe('<DropdownMenu />', () => {
-  const icon = 'my-icon';
-  const size = 123;
-  let items;
-  let wrapper;
-  let action;
-
-  beforeEach(() => {
-    action = sinon.spy();
-
-    items = [
-      { text: 'first item',  action: action, href: '/some/url' },
-      { text: 'second item', action: 'noop' },
-    ];
-    wrapper = shallow(<DropdownMenu icon={icon} items={items} size={size} />);
-  });
-
-  it('contains one <Dropdown />', () => {
-    expect(wrapper).to.have.exactly(1).descendants(Dropdown);
-  });
-
-  it('contains one <DropdownTrigger />', () => {
-    expect(wrapper.find(Dropdown)).to.have.exactly(1).descendants(DropdownTrigger);
-  });
-
-  it('contains one <DropdownContent />', () => {
-    expect(wrapper.find(Dropdown)).to.have.exactly(1).descendants(DropdownContent);
-  });
-
-  it('does not contain a <DropdownContent /> if isUserTouching', () => {
-    const touchingWrapper = shallow(<DropdownMenu icon={icon} items={items} size={size} isUserTouching={isTrue} />);
-    expect(touchingWrapper.find(Dropdown)).to.have.exactly(0).descendants(DropdownContent);
-  });
-
-  it('does not contain a <DropdownContent /> if isUserTouching', () => {
-    const touchingWrapper = shallow(<DropdownMenu icon={icon} items={items} size={size} isUserTouching={isTrue} />);
-    expect(touchingWrapper.find(Dropdown)).to.have.exactly(0).descendants(DropdownContent);
-  });
-
-  it('uses props.size for <DropdownTrigger /> style values', () => {
-    ['font-size', 'width', 'line-height'].map((property) => {
-      expect(wrapper.find(DropdownTrigger)).to.have.style(property, `${size}px`);
-    });
-  });
-
-  it('uses props.icon as icon class name', () => {
-    expect(wrapper.find(DropdownTrigger).find('i')).to.have.className(`fa-${icon}`);
-  });
-
-  it('is not expanded by default', () => {
-    expect(wrapper.state('expanded')).to.be.equal(false);
-  });
-
-  it('does not render the list elements if not expanded', () => {
-    const lis = wrapper.find(DropdownContent).find('li');
-    expect(lis.length).to.be.equal(0);
-  });
-
-  it('sets expanded to true when clicking the trigger', () => {
-    const wrapper = mount(<DropdownMenu icon={icon} items={items} size={size} />);
-    wrapper.find(DropdownTrigger).first().simulate('click');
-    expect(wrapper.state('expanded')).to.be.equal(true);
-  });
-
-  it('calls onModalOpen when clicking the trigger if isUserTouching', () => {
-    const onModalOpen = sinon.spy();
-    const touchingWrapper = mount(<DropdownMenu icon={icon} items={items} status={3.14} size={size} onModalOpen={onModalOpen} isUserTouching={isTrue} />);
-    touchingWrapper.find(DropdownTrigger).first().simulate('click');
-    expect(onModalOpen.calledOnce).to.be.equal(true);
-    expect(onModalOpen.args[0][0]).to.be.deep.equal({ status: 3.14, actions: items, onClick: touchingWrapper.node.handleClick });
-  });
-
-  it('calls onModalClose when clicking an action if isUserTouching and isModalOpen', () => {
-    const onModalOpen = sinon.spy();
-    const onModalClose = sinon.spy();
-    const touchingWrapper = mount(<DropdownMenu icon={icon} items={items} status={3.14} size={size} isModalOpen onModalOpen={onModalOpen} onModalClose={onModalClose} isUserTouching={isTrue} />);
-    touchingWrapper.find(DropdownTrigger).first().simulate('click');
-    touchingWrapper.node.handleClick({ currentTarget: { getAttribute: () => '0' }, preventDefault: () => null });
-    expect(onModalClose.calledOnce).to.be.equal(true);
-  });
-
-  // Error: ReactWrapper::state() can only be called on the root
-  /*it('sets expanded to false when clicking outside', () => {
-    const wrapper = mount((
-      <div>
-        <DropdownMenu icon={icon} items={items} size={size} />
-        <span />
-      </div>
-    ));
-
-    wrapper.find(DropdownTrigger).first().simulate('click');
-    expect(wrapper.find(DropdownMenu).first().state('expanded')).to.be.equal(true);
-
-    wrapper.find('span').first().simulate('click');
-    expect(wrapper.find(DropdownMenu).first().state('expanded')).to.be.equal(false);
-  })*/
-
-  it('renders list elements for each props.items if expanded', () => {
-    const wrapper = mount(<DropdownMenu icon={icon} items={items} size={size} />);
-    wrapper.find(DropdownTrigger).first().simulate('click');
-    const lis = wrapper.find(DropdownContent).find('li');
-    expect(lis.length).to.be.equal(items.length);
-  });
-
-  it('uses the href passed in via props.items', () => {
-    wrapper
-      .find(DropdownContent).find('li a')
-      .forEach((a, i) => expect(a).to.have.attr('href', items[i].href));
-  });
-
-  it('uses the text passed in via props.items', () => {
-    wrapper
-      .find(DropdownContent).find('li a')
-      .forEach((a, i) => expect(a).to.have.text(items[i].text));
-  });
-
-  it('uses the action passed in via props.items as click handler', () => {
-    const wrapper = mount(<DropdownMenu icon={icon} items={items} size={size} />);
-    wrapper.find(DropdownTrigger).first().simulate('click');
-    wrapper.find(DropdownContent).find('li a').first().simulate('click');
-    expect(action.calledOnce).to.equal(true);
-  });
-});
diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js
index e165b4519..6e73c9251 100644
--- a/spec/javascript/components/emojify.test.js
+++ b/spec/javascript/components/emojify.test.js
@@ -22,23 +22,23 @@ describe('emojify', () => {
 
   it('does unicode', () => {
     expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal(
-      '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":family_wwbb:" src="/emoji/1f469-1f469-1f466-1f466.svg" />');
-    expect(emojify('\uD83D\uDC68\uD83D\uDC69\uD83D\uDC67\uD83D\uDC67')).to.equal(
-      '<img draggable="false" class="emojione" alt="👨👩👧👧" title=":family_mwgg:" src="/emoji/1f468-1f469-1f467-1f467.svg" />');
-    expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('<img draggable="false" class="emojione" alt="👩👩👦" title=":family_wwb:" src="/emoji/1f469-1f469-1f466.svg" />');
+      '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
+    expect(emojify('👨‍👩‍👧‍👧')).to.equal(
+      '<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
+    expect(emojify('👩‍👩‍👦')).to.equal('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
     expect(emojify('\u2757')).to.equal(
       '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
   });
 
   it('does multiple unicode', () => {
     expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal(
-      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" />');
+      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
     expect(emojify('\u2757#\uFE0F\u20E3')).to.equal(
-      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" />');
+      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
     expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal(
-      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
+      '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
     expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal(
-      'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> bar');
+      'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
   });
 
   it('ignores unicode inside of tags', () => {
diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb
new file mode 100644
index 000000000..6503c83e3
--- /dev/null
+++ b/spec/lib/activitypub/activity/accept_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Accept do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Accept',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: {
+        id: 'bar',
+        type: 'Follow',
+        actor: ActivityPub::TagManager.instance.uri_for(recipient),
+        object: ActivityPub::TagManager.instance.uri_for(sender),
+      },
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      Fabricate(:follow_request, account: recipient, target_account: sender)
+      subject.perform
+    end
+
+    it 'creates a follow relationship' do
+      expect(recipient.following?(sender)).to be true
+    end
+
+    it 'removes the follow request' do
+      expect(recipient.requested?(sender)).to be false
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb
new file mode 100644
index 000000000..54dd52a60
--- /dev/null
+++ b/spec/lib/activitypub/activity/announce_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Announce do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+  let(:status)    { Fabricate(:status, account: recipient) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Announce',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(status),
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'creates a reblog by sender of status' do
+      expect(sender.reblogged?(status)).to be true
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/block_spec.rb b/spec/lib/activitypub/activity/block_spec.rb
new file mode 100644
index 000000000..23c8cc31c
--- /dev/null
+++ b/spec/lib/activitypub/activity/block_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Block do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Block',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(recipient),
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'creates a block from sender to recipient' do
+      expect(sender.blocking?(recipient)).to be true
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
new file mode 100644
index 000000000..cdd499150
--- /dev/null
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -0,0 +1,350 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Create do
+  let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Create',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: object_json,
+    }.with_indifferent_access
+  end
+
+  subject { described_class.new(json, sender) }
+
+  before do
+    stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
+    stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
+  end
+
+  describe '#perform' do
+    before do
+      subject.perform
+    end
+
+    context 'standalone' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.text).to eq 'Lorem ipsum'
+      end
+
+      it 'missing to/cc defaults to direct privacy' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'direct'
+      end
+    end
+
+    context 'public' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          to: 'https://www.w3.org/ns/activitystreams#Public',
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'public'
+      end
+    end
+
+    context 'unlisted' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          cc: 'https://www.w3.org/ns/activitystreams#Public',
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'unlisted'
+      end
+    end
+
+    context 'private' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          to: 'http://example.com/followers',
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'private'
+      end
+    end
+
+    context 'direct' do
+      let(:recipient) { Fabricate(:account) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          to: ActivityPub::TagManager.instance.uri_for(recipient),
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'direct'
+      end
+    end
+
+    context 'as a reply' do
+      let(:original_status) { Fabricate(:status) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.thread).to eq original_status
+        expect(status.reply?).to be true
+        expect(status.in_reply_to_account).to eq original_status.account
+        expect(status.conversation).to eq original_status.conversation
+      end
+    end
+
+    context 'with mentions' do
+      let(:recipient) { Fabricate(:account) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          tag: [
+            {
+              type: 'Mention',
+              href: ActivityPub::TagManager.instance.uri_for(recipient),
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.mentions.map(&:account)).to include(recipient)
+      end
+    end
+
+    context 'with mentions missing href' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          tag: [
+            {
+              type: 'Mention',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+        expect(status).to_not be_nil
+      end
+    end
+
+    context 'with media attachments' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          attachment: [
+            {
+              type: 'Document',
+              mime_type: 'image/png',
+              url: 'http://example.com/attachment.png',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
+      end
+    end
+
+    context 'with media attachments missing url' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          attachment: [
+            {
+              type: 'Document',
+              mime_type: 'image/png',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+        expect(status).to_not be_nil
+      end
+    end
+
+    context 'with hashtags' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          tag: [
+            {
+              type: 'Hashtag',
+              href: 'http://example.com/blah',
+              name: '#test',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.tags.map(&:name)).to include('test')
+      end
+    end
+
+    context 'with hashtags missing name' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          tag: [
+            {
+              type: 'Hashtag',
+              href: 'http://example.com/blah',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+        expect(status).to_not be_nil
+      end
+    end
+
+    context 'with emojis' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum :tinking:',
+          tag: [
+            {
+              type: 'Emoji',
+              href: 'http://example.com/emoji.png',
+              name: 'tinking',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.emojis.map(&:shortcode)).to include('tinking')
+      end
+    end
+
+    context 'with emojis missing name' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum :tinking:',
+          tag: [
+            {
+              type: 'Emoji',
+              href: 'http://example.com/emoji.png',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+        expect(status).to_not be_nil
+      end
+    end
+
+    context 'with emojis missing href' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum :tinking:',
+          tag: [
+            {
+              type: 'Emoji',
+              name: 'tinking',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+        expect(status).to_not be_nil
+      end
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb
new file mode 100644
index 000000000..38254e31c
--- /dev/null
+++ b/spec/lib/activitypub/activity/delete_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Delete do
+  let(:sender)    { Fabricate(:account, domain: 'example.com') }
+  let(:status)    { Fabricate(:status, account: sender, uri: 'foobar') }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Delete',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(status),
+      signature: 'foo',
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'deletes sender\'s status' do
+      expect(Status.find_by(id: status.id)).to be_nil
+    end
+  end
+
+  context 'when the status has been reblogged' do
+    describe '#perform' do
+      subject { described_class.new(json, sender) }
+      let(:reblogger) { Fabricate(:account) }
+      let(:follower)   { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+
+      before do
+        stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+        follower.follow!(reblogger)
+        Fabricate(:status, account: reblogger, reblog: status)
+        subject.perform
+      end
+
+      it 'deletes sender\'s status' do
+        expect(Status.find_by(id: status.id)).to be_nil
+      end
+
+      it 'sends delete activity to followers of rebloggers' do
+        # one for Delete original post, and one for Undo reblog (normal delivery)
+        expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice
+      end
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb
new file mode 100644
index 000000000..6bbacdbe6
--- /dev/null
+++ b/spec/lib/activitypub/activity/follow_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Follow do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Follow',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(recipient),
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    context 'unlocked account' do
+      before do
+        subject.perform
+      end
+
+      it 'creates a follow from sender to recipient' do
+        expect(sender.following?(recipient)).to be true
+      end
+
+      it 'does not create a follow request' do
+        expect(sender.requested?(recipient)).to be false
+      end
+    end
+
+    context 'locked account' do
+      before do
+        recipient.update(locked: true)
+        subject.perform
+      end
+
+      it 'does not create a follow from sender to recipient' do
+        expect(sender.following?(recipient)).to be false
+      end
+
+      it 'creates a follow request' do
+        expect(sender.requested?(recipient)).to be true
+      end
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/like_spec.rb b/spec/lib/activitypub/activity/like_spec.rb
new file mode 100644
index 000000000..b69615a9d
--- /dev/null
+++ b/spec/lib/activitypub/activity/like_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Like do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+  let(:status)    { Fabricate(:status, account: recipient) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Like',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(status),
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'creates a favourite from sender to status' do
+      expect(sender.favourited?(status)).to be true
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/reject_spec.rb b/spec/lib/activitypub/activity/reject_spec.rb
new file mode 100644
index 000000000..7fd95bcc6
--- /dev/null
+++ b/spec/lib/activitypub/activity/reject_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Reject do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Reject',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: {
+        id: 'bar',
+        type: 'Follow',
+        actor: ActivityPub::TagManager.instance.uri_for(recipient),
+        object: ActivityPub::TagManager.instance.uri_for(sender),
+      },
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      Fabricate(:follow_request, account: recipient, target_account: sender)
+      subject.perform
+    end
+
+    it 'does not create a follow relationship' do
+      expect(recipient.following?(sender)).to be false
+    end
+
+    it 'removes the follow request' do
+      expect(recipient.requested?(sender)).to be false
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/undo_spec.rb b/spec/lib/activitypub/activity/undo_spec.rb
new file mode 100644
index 000000000..14c68efe5
--- /dev/null
+++ b/spec/lib/activitypub/activity/undo_spec.rb
@@ -0,0 +1,107 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Undo do
+  let(:sender) { Fabricate(:account, domain: 'example.com') }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Undo',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: object_json,
+    }.with_indifferent_access
+  end
+
+  subject { described_class.new(json, sender) }
+
+  describe '#perform' do
+    context 'with Announce' do
+      let(:status) { Fabricate(:status) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Announce',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(status),
+        }
+      end
+
+      before do
+        Fabricate(:status, reblog: status, account: sender, uri: 'bar')
+      end
+
+      it 'deletes the reblog' do
+        subject.perform
+        expect(sender.reblogged?(status)).to be false
+      end
+    end
+
+    context 'with Block' do
+      let(:recipient) { Fabricate(:account) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Block',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(recipient),
+        }
+      end
+
+      before do
+        sender.block!(recipient)
+      end
+
+      it 'deletes block from sender to recipient' do
+        subject.perform
+        expect(sender.blocking?(recipient)).to be false
+      end
+    end
+
+    context 'with Follow' do
+      let(:recipient) { Fabricate(:account) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Follow',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(recipient),
+        }
+      end
+
+      before do
+        sender.follow!(recipient)
+      end
+
+      it 'deletes follow from sender to recipient' do
+        subject.perform
+        expect(sender.following?(recipient)).to be false
+      end
+    end
+
+    context 'with Like' do
+      let(:status) { Fabricate(:status) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Like',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(status),
+        }
+      end
+
+      before do
+        Fabricate(:favourite, account: sender, status: status)
+      end
+
+      it 'deletes favourite from sender to status' do
+        subject.perform
+        expect(sender.favourited?(status)).to be false
+      end
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb
new file mode 100644
index 000000000..ea308e35c
--- /dev/null
+++ b/spec/lib/activitypub/activity/update_spec.rb
@@ -0,0 +1,45 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Update do
+  let!(:sender) { Fabricate(:account) }
+
+  before do
+    stub_request(:get, actor_json[:outbox]).to_return(status: 404)
+    stub_request(:get, actor_json[:followers]).to_return(status: 404)
+    stub_request(:get, actor_json[:following]).to_return(status: 404)
+
+    sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender))
+  end
+
+  let(:modified_sender) do
+    sender.dup.tap do |modified_sender|
+      modified_sender.display_name = 'Totally modified now'
+    end
+  end
+
+  let(:actor_json) do
+    ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json
+  end
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Update',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: actor_json,
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'updates profile' do
+      expect(sender.reload.display_name).to eq 'Totally modified now'
+    end
+  end
+end
diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb
new file mode 100644
index 000000000..a4d6fe8c3
--- /dev/null
+++ b/spec/lib/activitypub/linked_data_signature_spec.rb
@@ -0,0 +1,82 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::LinkedDataSignature do
+  include JsonLdHelper
+
+  let!(:sender) { Fabricate(:account, uri: 'http://example.com/alice') }
+
+  let(:raw_json) do
+    {
+      '@context' => 'https://www.w3.org/ns/activitystreams',
+      'id' => 'http://example.com/hello-world',
+    }
+  end
+
+  let(:json) { raw_json.merge('signature' => signature) }
+
+  subject { described_class.new(json) }
+
+  describe '#verify_account!' do
+    context 'when signature matches' do
+      let(:raw_signature) do
+        {
+          'creator' => 'http://example.com/alice',
+          'created' => '2017-09-23T20:21:34Z',
+        }
+      end
+
+      let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
+
+      it 'returns creator' do
+        expect(subject.verify_account!).to eq sender
+      end
+    end
+
+    context 'when signature is missing' do
+      let(:signature) { nil }
+
+      it 'returns nil' do
+        expect(subject.verify_account!).to be_nil
+      end
+    end
+
+    context 'when signature is tampered' do
+      let(:raw_signature) do
+        {
+          'creator' => 'http://example.com/alice',
+          'created' => '2017-09-23T20:21:34Z',
+        }
+      end
+
+      let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') }
+
+      it 'returns nil' do
+        expect(subject.verify_account!).to be_nil
+      end
+    end
+  end
+
+  describe '#sign!' do
+    subject { described_class.new(raw_json).sign!(sender) }
+
+    it 'returns a hash' do
+      expect(subject).to be_a Hash
+    end
+
+    it 'contains signature' do
+      expect(subject['signature']).to be_a Hash
+      expect(subject['signature']['signatureValue']).to be_present
+    end
+
+    it 'can be verified again' do
+      expect(described_class.new(subject).verify_account!).to eq sender
+    end
+  end
+
+  def sign(from_account, options, document)
+    options_hash   = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT)))
+    document_hash  = Digest::SHA256.hexdigest(canonicalize(document))
+    to_be_verified = options_hash + document_hash
+    Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_verified))
+  end
+end
diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb
new file mode 100644
index 000000000..0d1665216
--- /dev/null
+++ b/spec/lib/activitypub/tag_manager_spec.rb
@@ -0,0 +1,125 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::TagManager do
+  include RoutingHelper
+
+  subject { described_class.instance }
+
+  describe '#url_for' do
+    it 'returns a string' do
+      account = Fabricate(:account)
+      expect(subject.url_for(account)).to be_a String
+    end
+  end
+
+  describe '#uri_for' do
+    it 'returns a string' do
+      account = Fabricate(:account)
+      expect(subject.uri_for(account)).to be_a String
+    end
+  end
+
+  describe '#to' do
+    it 'returns public collection for public status' do
+      status = Fabricate(:status, visibility: :public)
+      expect(subject.to(status)).to eq ['https://www.w3.org/ns/activitystreams#Public']
+    end
+
+    it 'returns followers collection for unlisted status' do
+      status = Fabricate(:status, visibility: :unlisted)
+      expect(subject.to(status)).to eq [account_followers_url(status.account)]
+    end
+
+    it 'returns followers collection for private status' do
+      status = Fabricate(:status, visibility: :private)
+      expect(subject.to(status)).to eq [account_followers_url(status.account)]
+    end
+
+    it 'returns URIs of mentions for direct status' do
+      status    = Fabricate(:status, visibility: :direct)
+      mentioned = Fabricate(:account)
+      status.mentions.create(account: mentioned)
+      expect(subject.to(status)).to eq [subject.uri_for(mentioned)]
+    end
+  end
+
+  describe '#cc' do
+    it 'returns followers collection for public status' do
+      status = Fabricate(:status, visibility: :public)
+      expect(subject.cc(status)).to eq [account_followers_url(status.account)]
+    end
+
+    it 'returns public collection for unlisted status' do
+      status = Fabricate(:status, visibility: :unlisted)
+      expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public']
+    end
+
+    it 'returns empty array for private status' do
+      status = Fabricate(:status, visibility: :private)
+      expect(subject.cc(status)).to eq []
+    end
+
+    it 'returns empty array for direct status' do
+      status = Fabricate(:status, visibility: :direct)
+      expect(subject.cc(status)).to eq []
+    end
+
+    it 'returns URIs of mentions for non-direct status' do
+      status    = Fabricate(:status, visibility: :public)
+      mentioned = Fabricate(:account)
+      status.mentions.create(account: mentioned)
+      expect(subject.cc(status)).to include(subject.uri_for(mentioned))
+    end
+  end
+
+  describe '#local_uri?' do
+    it 'returns false for non-local URI' do
+      expect(subject.local_uri?('http://example.com/123')).to be false
+    end
+
+    it 'returns true for local URIs' do
+      account = Fabricate(:account)
+      expect(subject.local_uri?(subject.uri_for(account))).to be true
+    end
+  end
+
+  describe '#uri_to_local_id' do
+    it 'returns the local ID' do
+      account = Fabricate(:account)
+      expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username
+    end
+  end
+
+  describe '#uri_to_resource' do
+    it 'returns the local account' do
+      account = Fabricate(:account)
+      expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account
+    end
+
+    it 'returns the remote account by matching URI without fragment part' do
+      account = Fabricate(:account, uri: 'https://example.com/123')
+      expect(subject.uri_to_resource('https://example.com/123#456', Account)).to eq account
+    end
+
+    it 'returns the local status for ActivityPub URI' do
+      status = Fabricate(:status)
+      expect(subject.uri_to_resource(subject.uri_for(status), Status)).to eq status
+    end
+
+    it 'returns the local status for OStatus tag: URI' do
+      status = Fabricate(:status)
+      expect(subject.uri_to_resource(OStatus::TagManager.instance.uri_for(status), Status)).to eq status
+    end
+
+    it 'returns the local status for OStatus StreamEntry URL' do
+      status = Fabricate(:status)
+      stream_entry_url = account_stream_entry_url(status.account, status.stream_entry)
+      expect(subject.uri_to_resource(stream_entry_url, Status)).to eq status
+    end
+
+    it 'returns the remote status by matching URI without fragment part' do
+      status = Fabricate(:status, uri: 'https://example.com/123')
+      expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status
+    end
+  end
+end
diff --git a/spec/lib/emoji_spec.rb b/spec/lib/emoji_spec.rb
deleted file mode 100644
index 04931ccfb..000000000
--- a/spec/lib/emoji_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe Emoji do
-  describe '#unicode' do
-    it 'returns a unicode for a shortcode' do
-      expect(Emoji.instance.unicode(':joy:')).to eq '😂'
-    end
-  end
-
-  describe '#names' do
-    it 'returns an array' do
-      expect(Emoji.instance.names).to be_an Array
-    end
-  end
-end
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
index dfe1d8b8f..71b6b78d2 100644
--- a/spec/lib/formatter_spec.rb
+++ b/spec/lib/formatter_spec.rb
@@ -89,6 +89,54 @@ RSpec.describe Formatter do
       end
     end
 
+    context 'matches a URL with Japanese path string' do
+      let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
+
+      it 'has valid URL' do
+        is_expected.to include 'href="https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC"'
+      end
+    end
+
+    context 'matches a URL with Korean path string' do
+      let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' }
+
+      it 'has valid URL' do
+        is_expected.to include 'href="https://ko.wikipedia.org/wiki/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD"'
+      end
+    end
+
+    context 'matches a URL with Simplified Chinese path string' do
+      let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
+
+      it 'has valid URL' do
+        is_expected.to include 'href="https://baike.baidu.com/item/%E4%B8%AD%E5%8D%8E%E4%BA%BA%E6%B0%91%E5%85%B1%E5%92%8C%E5%9B%BD"'
+      end
+    end
+
+    context 'matches a URL with Traditional Chinese path string' do
+      let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' }
+
+      it 'has valid URL' do
+        is_expected.to include 'href="https://zh.wikipedia.org/wiki/%E8%87%BA%E7%81%A3"'
+      end
+    end
+
+    context 'contains unsafe URL (XSS attack, visible part)' do
+      let(:text) { %q{http://example.com/b<del>b</del>} }
+
+      it 'has escaped HTML' do
+        is_expected.to include '&lt;del&gt;b&lt;/del&gt;'
+      end
+    end
+
+    context 'contains unsafe URL (XSS attack, invisible part)' do
+      let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
+
+      it 'has escaped HTML' do
+        is_expected.to include '&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;'
+      end
+    end
+
     context 'contains HTML (script tag)' do
       let(:text) { '<script>alert("Hello")</script>' }
 
@@ -175,14 +223,92 @@ RSpec.describe Formatter do
 
         include_examples 'encode and link URLs'
       end
+
+      context '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 'with emoji at the start' do
+          let(:text) { ':coolcat: Beep boop' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+
+        context 'with emoji in the middle' do
+          let(:text) { 'Beep :coolcat: boop' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+
+        context 'with concatenated emoji' do
+          let(:text) { ':coolcat::coolcat:' }
+
+          it 'does not touch the shortcodes' do
+            is_expected.to match(/:coolcat::coolcat:/)
+          end
+        end
+
+        context 'with emoji at the end' do
+          let(:text) { 'Beep boop :coolcat:' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/boop <img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+      end
     end
 
     context 'with remote status' do
-      let(:status) { Fabricate(:status, text: 'Beep boop', uri: 'beepboop') }
+      let(:status) { Fabricate(:status, account: remote_account, text: 'Beep boop') }
 
       it 'reformats' do
         is_expected.to eq 'Beep boop'
       end
+
+      context '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 'with emoji 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" alt=":coolcat:"/)
+          end
+        end
+
+        context 'with emoji 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" alt=":coolcat:"/)
+          end
+        end
+
+        context '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 'with emoji 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" alt=":coolcat:"/)
+          end
+        end
+      end
     end
   end
 
@@ -226,7 +352,7 @@ RSpec.describe Formatter do
     end
 
     context 'with remote status' do
-      let(:status)  { Fabricate(:status, text: '<script>alert("Hello")</script>', uri: 'beep boop') }
+      let(:status)  { Fabricate(:status, account: remote_account, text: '<script>alert("Hello")</script>') }
 
       it 'returns tag-stripped text' do
         is_expected.to eq ''
diff --git a/spec/lib/language_detector_spec.rb b/spec/lib/language_detector_spec.rb
index ec39cb6a0..d17026511 100644
--- a/spec/lib/language_detector_spec.rb
+++ b/spec/lib/language_detector_spec.rb
@@ -3,10 +3,10 @@
 require 'rails_helper'
 
 describe LanguageDetector do
-  describe 'prepared_text' do
+  describe 'prepare_text' do
     it 'returns unmodified string without special cases' do
       string = 'just a regular string'
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
 
       expect(result).to eq string
     end
@@ -14,33 +14,35 @@ describe LanguageDetector do
     it 'collapses spacing in strings' do
       string = 'The formatting   in    this is very        odd'
 
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
       expect(result).to eq 'The formatting in this is very odd'
     end
 
     it 'strips usernames from strings before detection' do
       string = '@username Yeah, very surreal...! also @friend'
 
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
       expect(result).to eq 'Yeah, very surreal...! also'
     end
 
     it 'strips URLs from strings before detection' do
       string = 'Our website is https://example.com and also http://localhost.dev'
 
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
       expect(result).to eq 'Our website is and also'
     end
 
     it 'strips #hashtags from strings before detection' do
       string = 'Hey look at all the #animals and #fish'
 
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
       expect(result).to eq 'Hey look at all the and'
     end
   end
 
-  describe 'to_iso_s' do
+  describe 'detect' do
+    let(:account_without_user_locale) { Fabricate(:user, locale: nil).account }
+
     it 'detects english language for basic strings' do
       strings = [
         "Hello and welcome to mastodon how are you today?",
@@ -48,7 +50,7 @@ describe LanguageDetector do
         "a lot of people just want to feel righteous all the time and that's all that matters",
       ]
       strings.each do |string|
-        result = described_class.new(string).to_iso_s
+        result = described_class.instance.detect(string, account_without_user_locale)
 
         expect(result).to eq(:en), string
       end
@@ -56,14 +58,14 @@ describe LanguageDetector do
 
     it 'detects spanish language' do
       string = 'Obtener un Hola y bienvenidos a Mastodon'
-      result = described_class.new(string).to_iso_s
+      result = described_class.instance.detect(string, account_without_user_locale)
 
       expect(result).to eq :es
     end
 
     describe 'when language can\'t be detected' do
       it 'uses nil when sent an empty document' do
-        result = described_class.new('').to_iso_s
+        result = described_class.instance.detect('', account_without_user_locale)
         expect(result).to eq nil
       end
 
@@ -73,7 +75,7 @@ describe LanguageDetector do
           cld_result = CLD3::NNetLanguageIdentifier.new(0, 2048).find_language(string)
           expect(cld_result).not_to eq :en
 
-          result = described_class.new(string).to_iso_s
+          result = described_class.instance.detect(string, account_without_user_locale)
 
           expect(result).to eq nil
         end
@@ -82,14 +84,13 @@ describe LanguageDetector do
       describe 'with an account' do
         it 'uses the account locale when present' do
           account = double(user_locale: 'fr')
-          result  = described_class.new('', account).to_iso_s
+          result  = described_class.instance.detect('', account)
 
           expect(result).to eq :fr
         end
 
         it 'uses nil when account is present but has no locale' do
-          account = double(user_locale: nil)
-          result  = described_class.new('', account).to_iso_s
+          result  = described_class.instance.detect('', account_without_user_locale)
 
           expect(result).to eq nil
         end
@@ -97,8 +98,7 @@ describe LanguageDetector do
 
       describe 'with an `en` default locale' do
         it 'uses nil for undetectable string' do
-          string = ''
-          result = described_class.new(string).to_iso_s
+          result = described_class.instance.detect('', account_without_user_locale)
 
           expect(result).to eq nil
         end
@@ -114,7 +114,7 @@ describe LanguageDetector do
 
         it 'uses nil for undetectable string' do
           string = ''
-          result = described_class.new(string).to_iso_s
+          result = described_class.instance.detect(string, account_without_user_locale)
 
           expect(result).to eq nil
         end
diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb
index b0cb8f019..00e6f09dc 100644
--- a/spec/lib/ostatus/atom_serializer_spec.rb
+++ b/spec/lib/ostatus/atom_serializer_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe OStatus::AtomSerializer do
       follow_request_salmon = serialize(follow_request)
 
       object_type = follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:activity]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity]
     end
 
     it 'appends activity:verb element with request_friend type' do
@@ -26,7 +26,7 @@ RSpec.describe OStatus::AtomSerializer do
       follow_request_salmon = serialize(follow_request)
 
       verb = follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:request_friend]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:request_friend]
     end
 
     it 'appends activity:object with target account' do
@@ -44,13 +44,13 @@ RSpec.describe OStatus::AtomSerializer do
     it 'adds namespaces' do
       element = serialize
 
-      expect(element['xmlns']).to eq TagManager::XMLNS
-      expect(element['xmlns:thr']).to eq TagManager::THR_XMLNS
-      expect(element['xmlns:activity']).to eq TagManager::AS_XMLNS
-      expect(element['xmlns:poco']).to eq TagManager::POCO_XMLNS
-      expect(element['xmlns:media']).to eq TagManager::MEDIA_XMLNS
-      expect(element['xmlns:ostatus']).to eq TagManager::OS_XMLNS
-      expect(element['xmlns:mastodon']).to eq TagManager::MTDN_XMLNS
+      expect(element['xmlns']).to eq OStatus::TagManager::XMLNS
+      expect(element['xmlns:thr']).to eq OStatus::TagManager::THR_XMLNS
+      expect(element['xmlns:activity']).to eq OStatus::TagManager::AS_XMLNS
+      expect(element['xmlns:poco']).to eq OStatus::TagManager::POCO_XMLNS
+      expect(element['xmlns:media']).to eq OStatus::TagManager::MEDIA_XMLNS
+      expect(element['xmlns:ostatus']).to eq OStatus::TagManager::OS_XMLNS
+      expect(element['xmlns:mastodon']).to eq OStatus::TagManager::MTDN_XMLNS
     end
   end
 
@@ -97,11 +97,23 @@ RSpec.describe OStatus::AtomSerializer do
 
       mentioned = element.nodes.find do |node|
         node.name == 'link' &&
-        node[:rel] == 'mentioned' &&
-        node['ostatus:object-type'] == TagManager::TYPES[:person]
+          node[:rel] == 'mentioned' &&
+          node['ostatus:object-type'] == OStatus::TagManager::TYPES[:person]
       end
+
       expect(mentioned[:href]).to eq 'https://cb6e6126.ngrok.io/users/username'
     end
+
+    it 'appends link elements for emojis' do
+      Fabricate(:custom_emoji)
+
+      status  = Fabricate(:status, text: ':coolcat:')
+      element = serialize(status)
+      emoji   = element.nodes.find { |node| node.name == 'link' && node[:rel] == 'emoji' }
+
+      expect(emoji[:name]).to eq 'coolcat'
+      expect(emoji[:href]).to_not be_blank
+    end
   end
 
   describe 'render' do
@@ -176,7 +188,7 @@ RSpec.describe OStatus::AtomSerializer do
       author = OStatus::AtomSerializer.new.author(account)
 
       object_type = author.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:person]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:person]
     end
 
     it 'appends email element with username and domain for local account' do
@@ -196,7 +208,7 @@ RSpec.describe OStatus::AtomSerializer do
 
       author = OStatus::AtomSerializer.new.author(account)
 
-      link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+      link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
       expect(link[:rel]).to eq 'alternate'
       expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username'
@@ -346,9 +358,9 @@ RSpec.describe OStatus::AtomSerializer do
         mentioned_person = entry.nodes.find do |node|
           node.name == 'link' &&
           node[:rel] == 'mentioned' &&
-          node['ostatus:object-type'] == TagManager::TYPES[:collection]
+          node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection]
         end
-        expect(mentioned_person[:href]).to eq TagManager::COLLECTIONS[:public]
+        expect(mentioned_person[:href]).to eq OStatus::TagManager::COLLECTIONS[:public]
       end
 
       it 'does not append link element for the public collection if status is not publicly visible' do
@@ -359,8 +371,8 @@ RSpec.describe OStatus::AtomSerializer do
         entry.nodes.each do |node|
           if node.name == 'link' &&
              node[:rel] == 'mentioned' &&
-             node['ostatus:object-type'] == TagManager::TYPES[:collection]
-            expect(mentioned_collection[:href]).not_to eq TagManager::COLLECTIONS[:public]
+             node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection]
+            expect(mentioned_collection[:href]).not_to eq OStatus::TagManager::COLLECTIONS[:public]
           end
         end
       end
@@ -403,10 +415,10 @@ RSpec.describe OStatus::AtomSerializer do
 
       it 'returns element whose rendered view triggers creation when processed' do
         remote_account = Account.create!(username: 'username')
-        remote_status = Fabricate(:status, account: remote_account)
-        remote_status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
+        remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z')
 
         entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true)
+        entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test
         xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote')
 
         remote_status.destroy!
@@ -415,12 +427,12 @@ RSpec.describe OStatus::AtomSerializer do
         account = Account.create!(
           domain: 'remote',
           username: 'username',
-          last_webfingered_at: Time.now.utc,
+          last_webfingered_at: Time.now.utc
         )
 
         ProcessFeedService.new.call(xml, account)
 
-        expect(Status.find_by(uri: "tag:remote,2000-01-01:objectId=#{remote_status.id}:objectType=Status")).to be_instance_of Status
+        expect(Status.find_by(uri: "https://remote/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status
       end
     end
 
@@ -464,12 +476,11 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends id element with unique tag' do
-      status = Fabricate(:status, reblog_of_id: nil)
-      status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
+      status = Fabricate(:status, reblog_of_id: nil, created_at: '2000-01-01T00:00:00Z')
 
       entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
 
-      expect(entry.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(entry.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends published element with created date' do
@@ -495,7 +506,7 @@ RSpec.describe OStatus::AtomSerializer do
       status = Fabricate(:status)
       entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
       object_type = entry.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:note]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:note]
     end
 
     it 'appends activity:verb element with object type' do
@@ -504,7 +515,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
 
       object_type = entry.nodes.find { |node| node.name == 'activity:verb' }
-      expect(object_type.text).to eq TagManager::VERBS[:post]
+      expect(object_type.text).to eq OStatus::TagManager::VERBS[:post]
     end
 
     it 'appends activity:object element with target if present' do
@@ -514,7 +525,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.entry(reblog.stream_entry)
 
       object = entry.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{reblogged.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{reblogged.account.to_param}/statuses/#{reblogged.id}"
     end
 
     it 'does not append activity:object element if target is not present' do
@@ -529,9 +540,9 @@ RSpec.describe OStatus::AtomSerializer do
 
       entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
 
-      link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+      link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
-      expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}"
+      expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
     it 'appends link element for itself' do
@@ -552,7 +563,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.entry(reply_status.stream_entry)
 
       in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{in_reply_to_status.id}:objectType=Status"
+      expect(in_reply_to[:ref]).to eq "https://cb6e6126.ngrok.io/users/#{in_reply_to_status.account.to_param}/statuses/#{in_reply_to_status.id}"
     end
 
     it 'does not append thr:in-reply-to element if not threaded' do
@@ -642,7 +653,7 @@ RSpec.describe OStatus::AtomSerializer do
 
       feed = OStatus::AtomSerializer.new.feed(account, [])
 
-      link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+      link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
       expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username'
     end
@@ -728,8 +739,8 @@ RSpec.describe OStatus::AtomSerializer do
       time_after = Time.now
 
       expect(block_salmon.id.text).to(
-        eq(TagManager.instance.unique_tag(time_before.utc, block.id, 'Block'))
-          .or(eq(TagManager.instance.unique_tag(time_after.utc, block.id, 'Block')))
+        eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block'))
+          .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block')))
       )
     end
 
@@ -758,7 +769,7 @@ RSpec.describe OStatus::AtomSerializer do
       block_salmon = OStatus::AtomSerializer.new.block_salmon(block)
 
       object_type = block_salmon.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:activity]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity]
     end
 
     it 'appends activity:verb element with block' do
@@ -767,7 +778,7 @@ RSpec.describe OStatus::AtomSerializer do
       block_salmon = OStatus::AtomSerializer.new.block_salmon(block)
 
       verb = block_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:block]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:block]
     end
 
     it 'appends activity:object element with target account' do
@@ -815,8 +826,8 @@ RSpec.describe OStatus::AtomSerializer do
       time_after = Time.now
 
       expect(unblock_salmon.id.text).to(
-        eq(TagManager.instance.unique_tag(time_before.utc, block.id, 'Block'))
-          .or(eq(TagManager.instance.unique_tag(time_after.utc, block.id, 'Block')))
+        eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block'))
+          .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block')))
       )
     end
 
@@ -845,7 +856,7 @@ RSpec.describe OStatus::AtomSerializer do
       unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block)
 
       object_type = unblock_salmon.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:activity]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity]
     end
 
     it 'appends activity:verb element with block' do
@@ -854,7 +865,7 @@ RSpec.describe OStatus::AtomSerializer do
       unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block)
 
       verb = unblock_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:unblock]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:unblock]
     end
 
     it 'appends activity:object element with target account' do
@@ -923,7 +934,7 @@ RSpec.describe OStatus::AtomSerializer do
       favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
 
       verb = favourite_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:favorite]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:favorite]
     end
 
     it 'appends activity:object element with status' do
@@ -933,7 +944,7 @@ RSpec.describe OStatus::AtomSerializer do
       favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
 
       object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends thr:in-reply-to element for status' do
@@ -944,7 +955,7 @@ RSpec.describe OStatus::AtomSerializer do
       favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
 
       in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
@@ -994,8 +1005,8 @@ RSpec.describe OStatus::AtomSerializer do
       time_after = Time.now
 
       expect(unfavourite_salmon.id.text).to(
-        eq(TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite'))
-          .or(eq(TagManager.instance.unique_tag(time_after.utc, favourite.id, 'Favourite')))
+        eq(OStatus::TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite'))
+          .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, favourite.id, 'Favourite')))
       )
     end
 
@@ -1023,7 +1034,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
 
       verb = unfavourite_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:unfavorite]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:unfavorite]
     end
 
     it 'appends activity:object element with status' do
@@ -1033,7 +1044,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
 
       object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends thr:in-reply-to element for status' do
@@ -1044,7 +1055,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
 
       in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
@@ -1106,7 +1117,7 @@ RSpec.describe OStatus::AtomSerializer do
       follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow)
 
       object_type = follow_salmon.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:activity]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity]
     end
 
     it 'appends activity:verb element with follow' do
@@ -1115,7 +1126,7 @@ RSpec.describe OStatus::AtomSerializer do
       follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow)
 
       verb = follow_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:follow]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:follow]
     end
 
     it 'appends activity:object element with target account' do
@@ -1179,8 +1190,8 @@ RSpec.describe OStatus::AtomSerializer do
       time_after = Time.now
 
       expect(unfollow_salmon.id.text).to(
-        eq(TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow'))
-          .or(eq(TagManager.instance.unique_tag(time_after.utc, follow.id, 'Follow')))
+        eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow'))
+          .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow.id, 'Follow')))
       )
     end
 
@@ -1223,7 +1234,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow)
 
       object_type = unfollow_salmon.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:activity]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity]
     end
 
     it 'appends activity:verb element with follow' do
@@ -1233,7 +1244,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow)
 
       verb = unfollow_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:unfollow]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:unfollow]
     end
 
     it 'appends activity:object element with target account' do
@@ -1327,8 +1338,8 @@ RSpec.describe OStatus::AtomSerializer do
       time_after = Time.now
 
       expect(authorize_follow_request_salmon.id.text).to(
-        eq(TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest'))
-          .or(eq(TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest')))
+        eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest'))
+          .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest')))
       )
     end
 
@@ -1348,7 +1359,7 @@ RSpec.describe OStatus::AtomSerializer do
       authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)
 
       object_type = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:activity]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity]
     end
 
     it 'appends activity:verb element with authorize' do
@@ -1357,7 +1368,7 @@ RSpec.describe OStatus::AtomSerializer do
       authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)
 
       verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:authorize]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize]
     end
 
     it 'returns element whose rendered view creates follow from follow request when processed' do
@@ -1396,8 +1407,8 @@ RSpec.describe OStatus::AtomSerializer do
       time_after = Time.now
 
       expect(reject_follow_request_salmon.id.text).to(
-        eq(TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest'))
-          .or(TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest'))
+        eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest'))
+          .or(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest'))
       )
     end
 
@@ -1413,14 +1424,14 @@ RSpec.describe OStatus::AtomSerializer do
       follow_request = Fabricate(:follow_request)
       reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)
       object_type = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:activity]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity]
     end
 
     it 'appends activity:verb element with authorize' do
       follow_request = Fabricate(:follow_request)
       reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)
       verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' }
-      expect(verb.text).to eq TagManager::VERBS[:reject]
+      expect(verb.text).to eq OStatus::TagManager::VERBS[:reject]
     end
 
     it 'returns element whose rendered view deletes follow request when processed' do
@@ -1452,7 +1463,7 @@ RSpec.describe OStatus::AtomSerializer do
     it 'appends id element with URL for status' do
       status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
       object = OStatus::AtomSerializer.new.object(status)
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends published element with created date' do
@@ -1462,7 +1473,8 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends updated element with updated date' do
-      status = Fabricate(:status, updated_at: '2000-01-01T00:00:00Z')
+      status = Fabricate(:status)
+      status.updated_at = '2000-01-01T00:00:00Z'
       object = OStatus::AtomSerializer.new.object(status)
       expect(object.updated.text).to eq '2000-01-01T00:00:00Z'
     end
@@ -1491,7 +1503,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.object(status)
 
       object_type = entry.nodes.find { |node| node.name == 'activity:object-type' }
-      expect(object_type.text).to eq TagManager::TYPES[:note]
+      expect(object_type.text).to eq OStatus::TagManager::TYPES[:note]
     end
 
     it 'appends activity:verb element with verb' do
@@ -1500,7 +1512,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.object(status)
 
       object_type = entry.nodes.find { |node| node.name == 'activity:verb' }
-      expect(object_type.text).to eq TagManager::VERBS[:post]
+      expect(object_type.text).to eq OStatus::TagManager::VERBS[:post]
     end
 
     it 'appends link element for an alternative' do
@@ -1509,7 +1521,7 @@ RSpec.describe OStatus::AtomSerializer do
 
       entry = OStatus::AtomSerializer.new.object(status)
 
-      link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+      link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
       expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
@@ -1522,7 +1534,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.object(reply)
 
       in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{thread.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{thread.account.to_param}/statuses/#{thread.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}"
     end
 
diff --git a/spec/lib/ostatus/tag_manager_spec.rb b/spec/lib/ostatus/tag_manager_spec.rb
new file mode 100644
index 000000000..31195bae2
--- /dev/null
+++ b/spec/lib/ostatus/tag_manager_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe OStatus::TagManager do
+  describe '#unique_tag' do
+    it 'returns a unique tag' do
+      expect(OStatus::TagManager.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status'
+    end
+  end
+
+  describe '#unique_tag_to_local_id' do
+    it 'returns the ID part' do
+      expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12'
+    end
+
+    it 'returns nil if it is not local id' do
+      expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to eq nil
+    end
+
+    it 'returns nil if it is not expected type' do
+      expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to eq nil
+    end
+
+    it 'returns nil if it does not have object ID' do
+      expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to eq nil
+    end
+  end
+
+  describe '#local_id?' do
+    it 'returns true for a local ID' do
+      expect(OStatus::TagManager.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true
+    end
+
+    it 'returns false for a foreign ID' do
+      expect(OStatus::TagManager.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false
+    end
+  end
+
+  describe '#uri_for' do
+    subject { OStatus::TagManager.instance.uri_for(target) }
+
+    context 'comment object' do
+      let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) }
+
+      it 'returns the unique tag for status' do
+        expect(target.object_type).to eq :comment
+        is_expected.to eq target.uri
+      end
+    end
+
+    context 'note object' do
+      let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) }
+
+      it 'returns the unique tag for status' do
+        expect(target.object_type).to eq :note
+        is_expected.to eq target.uri
+      end
+    end
+
+    context 'person object' do
+      let(:target) { Fabricate(:account, username: 'alice') }
+
+      it 'returns the URL for account' do
+        expect(target.object_type).to eq :person
+        is_expected.to eq 'https://cb6e6126.ngrok.io/users/alice'
+      end
+    end
+  end
+end
diff --git a/spec/lib/stream_entry_finder_spec.rb b/spec/lib/status_finder_spec.rb
index 64e03c36a..3ef086736 100644
--- a/spec/lib/stream_entry_finder_spec.rb
+++ b/spec/lib/status_finder_spec.rb
@@ -2,17 +2,17 @@
 
 require 'rails_helper'
 
-describe StreamEntryFinder do
+describe StatusFinder do
   include RoutingHelper
 
-  describe '#stream_entry' do
+  describe '#status' do
     context 'with a status url' do
       let(:status) { Fabricate(:status) }
       let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) }
       subject { described_class.new(url) }
 
       it 'finds the stream entry' do
-        expect(subject.stream_entry).to eq(status.stream_entry)
+        expect(subject.status).to eq(status)
       end
 
       it 'raises an error if action is not :show' do
@@ -20,7 +20,7 @@ describe StreamEntryFinder do
         expect(recognized).to receive(:[]).with(:action).and_return(:create)
         expect(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized)
 
-        expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+        expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
 
@@ -30,7 +30,17 @@ describe StreamEntryFinder do
       subject { described_class.new(url) }
 
       it 'finds the stream entry' do
-        expect(subject.stream_entry).to eq(stream_entry)
+        expect(subject.status).to eq(stream_entry.status)
+      end
+    end
+
+    context 'with a remote url even if id exists on local' do
+      let(:status) { Fabricate(:status) }
+      let(:url) { "https://example.com/users/test/statuses/#{status.id}" }
+      subject { described_class.new(url) }
+
+      it 'raises an error' do
+        expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
 
@@ -39,7 +49,7 @@ describe StreamEntryFinder do
       subject { described_class.new(url) }
 
       it 'raises an error' do
-        expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+        expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
 
@@ -48,7 +58,7 @@ describe StreamEntryFinder do
       subject { described_class.new(url) }
 
       it 'raises an error' do
-        expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+        expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
   end
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index 1fae6bec4..5427a2929 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -63,23 +63,23 @@ RSpec.describe TagManager do
 
   describe '#local_url?' do
     around do |example|
-      original_local_domain = Rails.configuration.x.local_domain
+      original_web_domain = Rails.configuration.x.web_domain
       example.run
-      Rails.configuration.x.local_domain = original_local_domain
+      Rails.configuration.x.web_domain = original_web_domain
     end
 
     it 'returns true if the normalized string with port is local URL' do
-      Rails.configuration.x.local_domain = 'domain:42'
+      Rails.configuration.x.web_domain = 'domain:42'
       expect(TagManager.instance.local_url?('https://DoMaIn:42/')).to eq true
     end
 
     it 'returns true if the normalized string without port is local URL' do
-      Rails.configuration.x.local_domain = 'domain'
+      Rails.configuration.x.web_domain = 'domain'
       expect(TagManager.instance.local_url?('https://DoMaIn/')).to eq true
     end
 
     it 'returns false for string with irrelevant characters' do
-      Rails.configuration.x.local_domain = 'domain'
+      Rails.configuration.x.web_domain = 'domain'
       expect(TagManager.instance.local_url?('https://domainn/')).to eq false
     end
   end
@@ -120,82 +120,6 @@ RSpec.describe TagManager do
     end
   end
 
-  describe '#unique_tag' do
-    it 'returns a unique tag' do
-      expect(TagManager.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status'
-    end
-  end
-
-  describe '#unique_tag_to_local_id' do
-    it 'returns the ID part' do
-      expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12'
-    end
-
-    it 'returns nil if it is not local id' do
-      expect(TagManager.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to eq nil
-    end
-
-    it 'returns nil if it is not expected type' do
-      expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to eq nil
-    end
-
-    it 'returns nil if it does not have object ID' do
-      expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to eq nil
-    end
-  end
-
-  describe '#local_id?' do
-    it 'returns true for a local ID' do
-      expect(TagManager.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true
-    end
-
-    it 'returns false for a foreign ID' do
-      expect(TagManager.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false
-    end
-  end
-
-  describe '#uri_for' do
-    subject { TagManager.instance.uri_for(target) }
-
-    context 'activity object' do
-      let(:target) { Fabricate(:status, reblog: Fabricate(:status)).stream_entry }
-
-      before { target.update!(created_at: '2000-01-01T00:00:00Z') }
-
-      it 'returns the unique tag for status' do
-        expect(target.object_type).to eq :activity
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
-      end
-    end
-
-    context 'comment object' do
-      let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) }
-
-      it 'returns the unique tag for status' do
-        expect(target.object_type).to eq :comment
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
-      end
-    end
-
-    context 'note object' do
-      let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) }
-
-      it 'returns the unique tag for status' do
-        expect(target.object_type).to eq :note
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
-      end
-    end
-
-    context 'person object' do
-      let(:target) { Fabricate(:account, username: 'alice') }
-
-      it 'returns the URL for account' do
-        expect(target.object_type).to eq :person
-        is_expected.to eq 'https://cb6e6126.ngrok.io/users/alice'
-      end
-    end
-  end
-
   describe '#url_for' do
     let(:alice) { Fabricate(:account, username: 'alice') }
 
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 17e2d8499..361577eff 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -642,7 +642,6 @@ RSpec.describe Account, type: :model do
       it 'returns remote accounts with followers whose subscription expiration date is past or not given' do
         local = Fabricate(:account, domain: nil)
         matches = [
-          { domain: 'remote', subscription_expires_at: nil },
           { domain: 'remote', subscription_expires_at: '2000-01-01T00:00:00Z' },
         ].map(&method(:Fabricate).curry(2).call(:account))
         matches.each(&local.method(:follow!))
diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb
new file mode 100644
index 000000000..ef957fc1d
--- /dev/null
+++ b/spec/models/concerns/account_interactions_spec.rb
@@ -0,0 +1,40 @@
+require 'rails_helper'
+
+describe AccountInteractions do
+  describe 'muting an account' do
+    before do
+      @me = Fabricate(:account, username: 'Me')
+      @you = Fabricate(:account, username: 'You')
+    end
+
+    context 'with the notifications option unspecified' do
+      before do
+        @me.mute!(@you)
+      end
+
+      it 'defaults to muting notifications' do
+        expect(@me.muting_notifications?(@you)).to be(true)
+      end
+    end
+
+    context 'with the notifications option set to false' do
+      before do
+        @me.mute!(@you, notifications: false)
+      end
+
+      it 'does not mute notifications' do
+        expect(@me.muting_notifications?(@you)).to be(false)
+      end
+    end
+
+    context 'with the notifications option set to true' do
+      before do
+        @me.mute!(@you, notifications: true)
+      end
+
+      it 'does mute notifications' do
+        expect(@me.muting_notifications?(@you)).to be(true)
+      end
+    end
+  end
+end
diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb
new file mode 100644
index 000000000..cb51e9519
--- /dev/null
+++ b/spec/models/custom_emoji_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+RSpec.describe CustomEmoji, type: :model do
+  describe '.from_text' do
+    let!(:emojo) { Fabricate(:custom_emoji) }
+
+    subject { described_class.from_text(text, nil) }
+
+    context 'with plain text' do
+      let(:text) { 'Hello :coolcat:' }
+
+      it 'returns records used via shortcodes in text' do
+        is_expected.to include(emojo)
+      end
+    end
+
+    context 'with html' do
+      let(:text) { '<p>Hello :coolcat:</p>' }
+
+      it 'returns records used via shortcodes in text' do
+        is_expected.to include(emojo)
+      end
+    end
+  end
+end
diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb
index fa52077cd..321761166 100644
--- a/spec/models/import_spec.rb
+++ b/spec/models/import_spec.rb
@@ -1,5 +1,24 @@
 require 'rails_helper'
 
 RSpec.describe Import, type: :model do
+  let (:account) { Fabricate(:account) }
+  let (:type) { 'following' }
+  let (:data) { attachment_fixture('imports.txt') }
 
+  describe 'validations' do
+    it 'has a valid parameters' do
+      import = Import.create(account: account, type: type, data: data)
+      expect(import).to be_valid
+    end
+
+    it 'is invalid without an type' do
+      import = Import.create(account: account, data: data)
+      expect(import).to model_have_error_on_field(:type)
+    end
+
+    it 'is invalid without a data' do
+      import = Import.create(account: account, type: type)
+      expect(import).to model_have_error_on_field(:data)
+    end
+  end
 end
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
index 6c2723845..d40ebf6dc 100644
--- a/spec/models/report_spec.rb
+++ b/spec/models/report_spec.rb
@@ -21,4 +21,18 @@ describe Report do
       expect(report.media_attachments).to eq [media_attachment]
     end
   end
+
+  describe 'validatiions' do
+    it 'has a valid fabricator' do
+      report = Fabricate(:report)
+      report.valid?
+      expect(report).to be_valid
+    end
+
+    it 'is invalid if comment is longer than 1000 characters' do
+      report = Fabricate.build(:report, comment: Faker::Lorem.characters(1001))
+      report.valid?
+      expect(report).to model_have_error_on_field(:comment)
+    end
+  end
 end
diff --git a/spec/models/site_upload_spec.rb b/spec/models/site_upload_spec.rb
new file mode 100644
index 000000000..8745d54b8
--- /dev/null
+++ b/spec/models/site_upload_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe SiteUpload, type: :model do
+
+end
diff --git a/spec/models/status_pin_spec.rb b/spec/models/status_pin_spec.rb
new file mode 100644
index 000000000..6f54f80f9
--- /dev/null
+++ b/spec/models/status_pin_spec.rb
@@ -0,0 +1,41 @@
+require 'rails_helper'
+
+RSpec.describe StatusPin, type: :model do
+  describe 'validations' do
+    it 'allows pins of own statuses' do
+      account = Fabricate(:account)
+      status  = Fabricate(:status, account: account)
+
+      expect(StatusPin.new(account: account, status: status).save).to be true
+    end
+
+    it 'does not allow pins of statuses by someone else' do
+      account = Fabricate(:account)
+      status  = Fabricate(:status)
+
+      expect(StatusPin.new(account: account, status: status).save).to be false
+    end
+
+    it 'does not allow pins of reblogs' do
+      account = Fabricate(:account)
+      status  = Fabricate(:status, account: account)
+      reblog  = Fabricate(:status, reblog: status)
+
+      expect(StatusPin.new(account: account, status: reblog).save).to be false
+    end
+
+    it 'does not allow pins of private statuses' do
+      account = Fabricate(:account)
+      status  = Fabricate(:status, account: account, visibility: :private)
+
+      expect(StatusPin.new(account: account, status: status).save).to be false
+    end
+
+    it 'does not allow pins of direct statuses' do
+      account = Fabricate(:account)
+      status  = Fabricate(:status, account: account, visibility: :direct)
+
+      expect(StatusPin.new(account: account, status: status).save).to be false
+    end
+  end
+end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 626fc3f98..9cb71d715 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -13,9 +13,15 @@ RSpec.describe Status, type: :model do
     end
 
     it 'returns false if a remote URI is set' do
-      subject.uri = 'a'
+      alice.update(domain: 'example.com')
+      subject.save
       expect(subject.local?).to be false
     end
+
+    it 'returns true if a URI is set and `local` is true' do
+      subject.update(uri: 'example.com', local: true)
+      expect(subject.local?).to be true
+    end
   end
 
   describe '#reblog?' do
@@ -167,16 +173,19 @@ RSpec.describe Status, type: :model do
     end
   end
 
-  describe '.local_only' do
-    it 'returns only statuses from local accounts' do
-      local_account = Fabricate(:account, domain: nil)
-      remote_account = Fabricate(:account, domain: 'test.com')
-      local_status = Fabricate(:status, account: local_account)
-      remote_status = Fabricate(:status, account: remote_account)
+  describe '.not_in_filtered_languages' do
+    context 'for accounts with language filters' do
+      let(:user) { Fabricate(:user, filtered_languages: ['en']) }
+
+      it 'does not include statuses in filtered languages' do
+        status = Fabricate(:status, language: 'en')
+        expect(Status.not_in_filtered_languages(user.account)).not_to include status
+      end
 
-      results = described_class.local_only
-      expect(results).to include(local_status)
-      expect(results).not_to include(remote_status)
+      it 'includes status with unknown language' do
+        status = Fabricate(:status, language: nil)
+        expect(Status.not_in_filtered_languages(user.account)).to include status
+      end
     end
   end
 
@@ -495,7 +504,7 @@ RSpec.describe Status, type: :model do
     end
   end
 
-  describe 'before_create' do
+  describe 'before_validation' do
     it 'sets account being replied to correctly over intermediary nodes' do
       first_status = Fabricate(:status, account: bob)
       intermediary = Fabricate(:status, thread: first_status, account: alice)
@@ -512,5 +521,30 @@ RSpec.describe Status, type: :model do
       parent = Fabricate(:status, text: 'First')
       expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id
     end
+
+    it 'sets `local` to true for status by local account' do
+      expect(Status.create(account: alice, text: 'foo').local).to be true
+    end
+
+    it 'sets `local` to false for status by remote account' do
+      alice.update(domain: 'example.com')
+      expect(Status.create(account: alice, text: 'foo').local).to be false
+    end
+  end
+
+  describe 'validation' do
+    it 'disallow empty uri for remote status' do
+      alice.update(domain: 'example.com')
+      status = Fabricate.build(:status, uri: '', account: alice)
+      expect(status).to model_have_error_on_field(:uri)
+    end
+  end
+
+  describe 'after_create' do
+    it 'saves ActivityPub uri as uri for local status' do
+      status = Status.create(account: alice, text: 'foo')
+      status.reload
+      expect(status.uri).to start_with('https://')
+    end
   end
 end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ef45818b9..99aeca01b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -286,4 +286,24 @@ RSpec.describe User, type: :model do
       Fabricate(:user)
     end
   end
+
+  describe 'token_for_app' do
+    let(:user) { Fabricate(:user) }
+    let(:app) { Fabricate(:application, owner: user) }
+
+    it 'returns a token' do
+      expect(user.token_for_app(app)).to be_a(Doorkeeper::AccessToken)
+    end
+
+    it 'persists a token' do
+      t = user.token_for_app(app)
+      expect(user.token_for_app(app)).to eql(t)
+    end
+
+    it 'is nil if user does not own app' do
+      app.update!(owner: nil)
+
+      expect(user.token_for_app(app)).to be_nil
+    end
+  end
 end
diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb
new file mode 100644
index 000000000..ed7e9bba8
--- /dev/null
+++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb
@@ -0,0 +1,123 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::FetchRemoteAccountService do
+  subject { ActivityPub::FetchRemoteAccountService.new }
+
+  let!(:actor) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'https://example.com/alice',
+      type: 'Person',
+      preferredUsername: 'alice',
+      name: 'Alice',
+      summary: 'Foo bar',
+      inbox: 'http://example.com/alice/inbox',
+    }
+  end
+
+  describe '#call' do
+    let(:account) { subject.call('https://example.com/alice') }
+
+    shared_examples 'sets profile data' do
+      it 'returns an account' do
+        expect(account).to be_an Account
+      end
+
+      it 'sets display name' do
+        expect(account.display_name).to eq 'Alice'
+      end
+
+      it 'sets note' do
+        expect(account.note).to eq 'Foo bar'
+      end
+
+      it 'sets URL' do
+        expect(account.url).to eq 'https://example.com/alice'
+      end
+    end
+
+    context 'when the account does not have a inbox' do
+      let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+      before do
+        actor[:inbox] = nil
+
+        stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+        stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+      end
+
+      it 'fetches resource' do
+        account
+        expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+      end
+
+      it 'looks up webfinger' do
+        account
+        expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+      end
+
+      it 'returns nil' do
+        expect(account).to be_nil
+      end
+
+    end
+
+    context 'when URI and WebFinger share the same host' do
+      let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+      before do
+        stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+        stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+      end
+
+      it 'fetches resource' do
+        account
+        expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+      end
+
+      it 'looks up webfinger' do
+        account
+        expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+      end
+
+      it 'sets username and domain from webfinger' do
+        expect(account.username).to eq 'alice'
+        expect(account.domain).to eq 'example.com'
+      end
+
+      include_examples 'sets profile data'
+    end
+
+    context 'when WebFinger presents different domain than URI' do
+      let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+      before do
+        stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+        stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+        stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+      end
+
+      it 'fetches resource' do
+        account
+        expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+      end
+
+      it 'looks up webfinger' do
+        account
+        expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+      end
+
+      it 'looks up "redirected" webfinger' do
+        account
+        expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
+      end
+
+      it 'sets username and domain from final webfinger' do
+        expect(account.username).to eq 'alice'
+        expect(account.domain).to eq 'iscool.af'
+      end
+
+      include_examples 'sets profile data'
+    end
+  end
+end
diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb
new file mode 100644
index 000000000..3b22257ed
--- /dev/null
+++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb
@@ -0,0 +1,75 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::FetchRemoteStatusService do
+  let(:sender) { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+  let(:valid_domain) { Rails.configuration.x.local_domain }
+
+  let(:note) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: "https://#{valid_domain}/@foo/1234",
+      type: 'Note',
+      content: 'Lorem ipsum',
+      attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
+    }
+  end
+
+  let(:create) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: "https://#{valid_domain}/@foo/1234/activity",
+      type: 'Create',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: note,
+    }
+  end
+
+  subject { described_class.new }
+
+  describe '#call' do
+    before do
+      subject.call(object[:id], Oj.dump(object))
+    end
+
+    context 'with Note object' do
+      let(:object) { note }
+
+      it 'creates status' do
+        status = sender.statuses.first
+        
+        expect(status).to_not be_nil
+        expect(status.text).to eq 'Lorem ipsum'
+      end
+    end
+
+    context 'with Create activity' do
+      let(:object) { create }
+
+      it 'creates status' do
+        status = sender.statuses.first
+        
+        expect(status).to_not be_nil
+        expect(status.text).to eq 'Lorem ipsum'
+      end
+    end
+
+    context 'with Announce activity' do
+      let(:status) { Fabricate(:status, account: recipient) }
+
+      let(:object) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          id: "https://#{valid_domain}/@foo/1234/activity",
+          type: 'Announce',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(status),
+        }
+      end
+
+      it 'creates a reblog by sender of status' do
+        expect(sender.reblogged?(status)).to be true
+      end
+    end
+  end
+end
diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb
new file mode 100644
index 000000000..84a74c231
--- /dev/null
+++ b/spec/services/activitypub/process_account_service_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::ProcessAccountService do
+  pending
+end
diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb
new file mode 100644
index 000000000..c1cc22523
--- /dev/null
+++ b/spec/services/activitypub/process_collection_service_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::ProcessCollectionService do
+  let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
+
+  let(:payload) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Create',
+      actor: ActivityPub::TagManager.instance.uri_for(actor),
+      object: {
+        id: 'bar',
+        type: 'Note',
+        content: 'Lorem ipsum',
+      },
+    }
+  end
+
+  let(:json) { Oj.dump(payload) }
+
+  subject { described_class.new }
+
+  describe '#call' do
+    context 'when actor is the sender'
+    context 'when actor differs from sender' do
+      let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
+
+      it 'processes payload with sender if no signature exists' do
+        expect_any_instance_of(ActivityPub::LinkedDataSignature).not_to receive(:verify_account!)
+        expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), forwarder)
+
+        subject.call(json, forwarder)
+      end
+
+      it 'processes payload with actor if valid signature exists' do
+        payload['signature'] = {'type' => 'RsaSignature2017'}
+
+        expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(actor)
+        expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor)
+
+        subject.call(json, forwarder)
+      end
+
+      it 'does not process payload if invalid signature exists' do
+        payload['signature'] = {'type' => 'RsaSignature2017'}
+
+        expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
+        expect(ActivityPub::Activity).not_to receive(:factory)
+
+        subject.call(json, forwarder)
+      end
+    end
+  end
+end
diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb
index 3f3a2bc56..6ea4d83da 100644
--- a/spec/services/authorize_follow_service_spec.rb
+++ b/spec/services/authorize_follow_service_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe AuthorizeFollowService do
     end
   end
 
-  describe 'remote' do
+  describe 'remote OStatus' do
     let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
 
     before do
@@ -42,8 +42,30 @@ RSpec.describe AuthorizeFollowService do
     it 'sends a follow request authorization salmon slap' do
       expect(a_request(:post, "http://salmon.example.com/").with { |req|
         xml = OStatus2::Salmon.new.unpack(req.body)
-        xml.match(TagManager::VERBS[:authorize])
+        xml.match(OStatus::TagManager::VERBS[:authorize])
       }).to have_been_made.once
     end
   end
+
+  describe 'remote ActivityPub' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
+
+    before do
+      FollowRequest.create(account: bob, target_account: sender)
+      stub_request(:post, bob.inbox_url).to_return(status: 200)
+      subject.call(bob, sender)
+    end
+
+    it 'removes follow request' do
+      expect(bob.requested?(sender)).to be false
+    end
+
+    it 'creates follow relation' do
+      expect(bob.following?(sender)).to be true
+    end
+
+    it 'sends an accept activity' do
+      expect(a_request(:post, bob.inbox_url)).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb
index c20085e25..f5c9adfb5 100644
--- a/spec/services/batched_remove_status_service_spec.rb
+++ b/spec/services/batched_remove_status_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe BatchedRemoveStatusService do
   let!(:alice)  { Fabricate(:account) }
   let!(:bob)    { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
   let!(:jeff)   { Fabricate(:account) }
+  let!(:hank)   { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
 
   let(:status1) { PostStatusService.new.call(alice, 'Hello @bob@example.com') }
   let(:status2) { PostStatusService.new.call(alice, 'Another status') }
@@ -15,9 +16,11 @@ RSpec.describe BatchedRemoveStatusService do
 
     stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {})
     stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {})
+    stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
 
     Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now)
     jeff.follow!(alice)
+    hank.follow!(alice)
 
     status1
     status2
@@ -45,17 +48,20 @@ RSpec.describe BatchedRemoveStatusService do
     expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once)
   end
 
-  it 'sends PuSH update to PuSH subscribers with two payloads united' do
+  it 'sends PuSH update to PuSH subscribers' do
     expect(a_request(:post, 'http://example.com/push').with { |req|
-      matches = req.body.scan(TagManager::VERBS[:delete])
-      matches.size == 2
-    }).to have_been_made
+      matches = req.body.match(OStatus::TagManager::VERBS[:delete])
+    }).to have_been_made.at_least_once
   end
 
   it 'sends Salmon slap to previously mentioned users' do
     expect(a_request(:post, "http://example.com/salmon").with { |req|
       xml = OStatus2::Salmon.new.unpack(req.body)
-      xml.match(TagManager::VERBS[:delete])
+      xml.match(OStatus::TagManager::VERBS[:delete])
     }).to have_been_made.once
   end
+
+  it 'sends delete activity to followers' do
+    expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once
+  end
 end
diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb
index 2a54e032e..c69ff7804 100644
--- a/spec/services/block_service_spec.rb
+++ b/spec/services/block_service_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe BlockService do
     end
   end
 
-  describe 'remote' do
+  describe 'remote OStatus' do
     let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
 
     before do
@@ -32,8 +32,25 @@ RSpec.describe BlockService do
     it 'sends a block salmon slap' do
       expect(a_request(:post, "http://salmon.example.com/").with { |req|
         xml = OStatus2::Salmon.new.unpack(req.body)
-        xml.match(TagManager::VERBS[:block])
+        xml.match(OStatus::TagManager::VERBS[:block])
       }).to have_been_made.once
     end
   end
+
+  describe 'remote ActivityPub' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
+
+    before do
+      stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+      subject.call(sender, bob)
+    end
+
+    it 'creates a blocking relation' do
+      expect(sender.blocking?(bob)).to be true
+    end
+
+    it 'sends a block activity' do
+      expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/bootstrap_timeline_service_spec.rb b/spec/services/bootstrap_timeline_service_spec.rb
new file mode 100644
index 000000000..5189b1de8
--- /dev/null
+++ b/spec/services/bootstrap_timeline_service_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+RSpec.describe BootstrapTimelineService do
+  subject { described_class.new }
+
+  describe '#call' do
+    let(:source_account) { Fabricate(:account) }
+
+    context 'when setting is empty' do
+      let!(:admin) { Fabricate(:user, admin: true) }
+
+      before do
+        Setting.bootstrap_timeline_accounts = nil
+        subject.call(source_account)
+      end
+
+      it 'follows admin accounts from account' do
+        expect(source_account.following?(admin.account)).to be true
+      end
+    end
+
+    context 'when setting is set' do
+      let!(:alice) { Fabricate(:account, username: 'alice') }
+      let!(:bob)   { Fabricate(:account, username: 'bob') }
+
+      before do
+        Setting.bootstrap_timeline_accounts = 'alice, bob'
+        subject.call(source_account)
+      end
+
+      it 'follows found accounts from account' do
+        expect(source_account.following?(alice)).to be true
+        expect(source_account.following?(bob)).to be true
+      end
+    end
+  end
+end
diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb
index 36f1b64d4..5bf2c74a9 100644
--- a/spec/services/favourite_service_spec.rb
+++ b/spec/services/favourite_service_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe FavouriteService do
     end
   end
 
-  describe 'remote' do
-    let(:bob)    { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+  describe 'remote OStatus' do
+    let(:bob)    { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
     let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com:blahblah') }
 
     before do
@@ -34,8 +34,26 @@ RSpec.describe FavouriteService do
     it 'sends a salmon slap' do
       expect(a_request(:post, "http://salmon.example.com/").with { |req|
         xml = OStatus2::Salmon.new.unpack(req.body)
-        xml.match(TagManager::VERBS[:favorite])
+        xml.match(OStatus::TagManager::VERBS[:favorite])
       }).to have_been_made.once
     end
   end
+
+  describe 'remote ActivityPub' do
+    let(:bob)    { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :activitypub, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
+    let(:status) { Fabricate(:status, account: bob) }
+
+    before do
+      stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {})
+      subject.call(sender, status)
+    end
+
+    it 'creates a favourite' do
+      expect(status.favourites.first).to_not be_nil
+    end
+
+    it 'sends a like activity' do
+      expect(a_request(:post, "http://example.com/inbox")).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index 698eb0324..ba61d22c3 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe FetchLinkCardService do
     stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt'))
     stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
     stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
+    stub_request(:head, 'http://example.com/日本語').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
+    stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt'))
     stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404)
 
     subject.call(status)
@@ -31,7 +33,7 @@ RSpec.describe FetchLinkCardService do
 
       it 'works with SJIS' do
         expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once
-        expect(status.preview_card.title).to eq("SJISのページ")
+        expect(status.preview_cards.first.title).to eq("SJISのページ")
       end
     end
 
@@ -40,7 +42,7 @@ RSpec.describe FetchLinkCardService do
 
       it 'works with SJIS even with wrong charset header' do
         expect(a_request(:get, 'http://example.com/sjis_with_wrong_charset')).to have_been_made.at_least_once
-        expect(status.preview_card.title).to eq("SJISのページ")
+        expect(status.preview_cards.first.title).to eq("SJISのページ")
       end
     end
 
@@ -49,13 +51,22 @@ RSpec.describe FetchLinkCardService do
 
       it 'works with koi8-r' do
         expect(a_request(:get, 'http://example.com/koi8-r')).to have_been_made.at_least_once
-        expect(status.preview_card.title).to eq("Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.")
+        expect(status.preview_cards.first.title).to eq("Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.")
+      end
+    end
+
+    context do
+      let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') }
+
+      it 'works with Japanese path string' do
+        expect(a_request(:get, 'http://example.com/日本語')).to have_been_made.at_least_once
+        expect(status.preview_cards.first.title).to eq("SJISのページ")
       end
     end
   end
 
   context 'in a remote status' do
-    let(:status) { Fabricate(:status, uri: 'abc', text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen?   Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }
+    let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen?   Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }
 
     it 'parses out URLs' do
       expect(a_request(:head, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once
diff --git a/spec/services/fetch_remote_resource_service_spec.rb b/spec/services/fetch_remote_resource_service_spec.rb
index 81b0e48e3..c14fcfc4e 100644
--- a/spec/services/fetch_remote_resource_service_spec.rb
+++ b/spec/services/fetch_remote_resource_service_spec.rb
@@ -30,7 +30,7 @@ describe FetchRemoteResourceService do
 
       _result = subject.call(url)
 
-      expect(account_service).to have_received(:call).with(feed_url, feed_content)
+      expect(account_service).to have_received(:call).with(feed_url, feed_content, nil)
     end
 
     it 'fetches remote statuses for entry types' do
@@ -47,7 +47,7 @@ describe FetchRemoteResourceService do
 
       _result = subject.call(url)
 
-      expect(account_service).to have_received(:call).with(feed_url, feed_content)
+      expect(account_service).to have_received(:call).with(feed_url, feed_content, nil)
     end
   end
 end
diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb
index 32dedb3ad..ceb39e5e6 100644
--- a/spec/services/follow_service_spec.rb
+++ b/spec/services/follow_service_spec.rb
@@ -44,9 +44,9 @@ RSpec.describe FollowService do
     end
   end
 
-  context 'remote account' do
+  context 'remote OStatus account' do
     describe 'locked account' do
-      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
 
       before do
         stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
@@ -60,13 +60,13 @@ RSpec.describe FollowService do
       it 'sends a follow request salmon slap' do
         expect(a_request(:post, "http://salmon.example.com/").with { |req|
           xml = OStatus2::Salmon.new.unpack(req.body)
-          xml.match(TagManager::VERBS[:request_friend])
+          xml.match(OStatus::TagManager::VERBS[:request_friend])
         }).to have_been_made.once
       end
     end
 
     describe 'unlocked account' do
-      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account }
+      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account }
 
       before do
         stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
@@ -81,7 +81,7 @@ RSpec.describe FollowService do
       it 'sends a follow salmon slap' do
         expect(a_request(:post, "http://salmon.example.com/").with { |req|
           xml = OStatus2::Salmon.new.unpack(req.body)
-          xml.match(TagManager::VERBS[:follow])
+          xml.match(OStatus::TagManager::VERBS[:follow])
         }).to have_been_made.once
       end
 
@@ -91,7 +91,7 @@ RSpec.describe FollowService do
     end
 
     describe 'already followed account' do
-      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account }
+      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account }
 
       before do
         sender.follow!(bob)
@@ -111,4 +111,21 @@ RSpec.describe FollowService do
       end
     end
   end
+
+  context 'remote ActivityPub account' do
+    let(:bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
+
+    before do
+      stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {})
+      subject.call(sender, bob.acct)
+    end
+
+    it 'creates follow request' do
+      expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil
+    end
+
+    it 'sends a follow activity to the inbox' do
+      expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb
index 8097cb250..800140b6f 100644
--- a/spec/services/mute_service_spec.rb
+++ b/spec/services/mute_service_spec.rb
@@ -32,4 +32,36 @@ RSpec.describe MuteService do
       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 {
+        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
+
+    it 'mutes notifications from the account' do
+      is_expected.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
+
+    it 'does not mute notifications from the account' do
+      is_expected.to_not change {
+        account.muting_notifications?(target_account)
+      }.from(false)
+    end
+  end
 end
diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb
index 7a66bd0fe..7088ae9d1 100644
--- a/spec/services/notify_service_spec.rb
+++ b/spec/services/notify_service_spec.rb
@@ -17,6 +17,16 @@ RSpec.describe NotifyService do
     is_expected.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)
+  end
+
+  it 'does notify when sender is muted without hide_notifications' do
+    recipient.mute!(sender, notifications: false)
+    is_expected.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)
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 57876dcc2..91902ff69 100644
--- a/spec/services/post_status_service_spec.rb
+++ b/spec/services/post_status_service_spec.rb
@@ -65,15 +65,12 @@ RSpec.describe PostStatusService do
   end
 
   it 'creates a status with a language set' do
-    detector = double(to_iso_s: :en)
-    allow(LanguageDetector).to receive(:new).and_return(detector)
-
     account = Fabricate(:account)
-    text = 'test status text'
+    text = 'This is an English text.'
 
-    subject.call(account, text)
+    status = subject.call(account, text)
 
-    expect(LanguageDetector).to have_received(:new).with(text, account)
+    expect(status.language).to eq 'en'
   end
 
   it 'processes mentions' do
@@ -100,16 +97,18 @@ RSpec.describe PostStatusService do
     expect(hashtags_service).to have_received(:call).with(status)
   end
 
-  it 'pings PuSH hubs' do
+  it 'gets distributed' do
     allow(DistributionWorker).to receive(:perform_async)
     allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async)
+    allow(ActivityPub::DistributionWorker).to receive(:perform_async)
+
     account = Fabricate(:account)
 
     status = subject.call(account, "test status update")
 
     expect(DistributionWorker).to have_received(:perform_async).with(status.id)
-    expect(Pubsubhubbub::DistributionWorker).
-      to have_received(:perform_async).with(status.stream_entry.id)
+    expect(Pubsubhubbub::DistributionWorker).to have_received(:perform_async).with(status.stream_entry.id)
+    expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id)
   end
 
   it 'crawls links' do
diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb
index 5e34370ee..aca675dc6 100644
--- a/spec/services/process_feed_service_spec.rb
+++ b/spec/services/process_feed_service_spec.rb
@@ -124,8 +124,7 @@ RSpec.describe ProcessFeedService do
 </entry>
 XML
 
-    stub_request(:head, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, headers: { 'Content-Type' => 'application/atom+xml' })
-    stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body)
+    stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body, headers: { 'Content-Type' => 'application/atom+xml' })
 
     bad_actor = Fabricate(:account, username: 'sombra', domain: 'talon.xyz')
 
@@ -168,7 +167,7 @@ XML
   end
 
   it 'ignores reblogs if it failed to retreive reblogged statuses' do
-    stub_request(:head, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404)
+    stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404)
 
     actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
 
diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb
index 984d13746..09f8fa45b 100644
--- a/spec/services/process_mentions_service_spec.rb
+++ b/spec/services/process_mentions_service_spec.rb
@@ -1,22 +1,44 @@
 require 'rails_helper'
 
 RSpec.describe ProcessMentionsService do
-  let(:account)     { Fabricate(:account, username: 'alice') }
-  let(:remote_user) { Fabricate(:account, username: 'remote_user', domain: 'example.com', salmon_url: 'http://salmon.example.com') }
-  let(:status)      { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") }
+  let(:account) { Fabricate(:account, username: 'alice') }
+  let(:status)  { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") }
 
-  subject { ProcessMentionsService.new }
+  context 'OStatus' do
+    let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
 
-  before do
-    stub_request(:post, remote_user.salmon_url)
-    subject.(status)
-  end
+    subject { ProcessMentionsService.new }
+
+    before do
+      stub_request(:post, remote_user.salmon_url)
+      subject.call(status)
+    end
 
-  it 'creates a mention' do
-    expect(remote_user.mentions.where(status: status).count).to eq 1
+    it 'creates a mention' do
+      expect(remote_user.mentions.where(status: status).count).to eq 1
+    end
+
+    it 'posts to remote user\'s Salmon end point' do
+      expect(a_request(:post, remote_user.salmon_url)).to have_been_made.once
+    end
   end
 
-  it 'posts to remote user\'s Salmon end point' do
-    expect(a_request(:post, remote_user.salmon_url)).to have_been_made
+  context 'ActivityPub' 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
+
+    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
diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb
index 5f89169e9..0ad5c5f6b 100644
--- a/spec/services/reblog_service_spec.rb
+++ b/spec/services/reblog_service_spec.rb
@@ -2,22 +2,49 @@ require 'rails_helper'
 
 RSpec.describe ReblogService do
   let(:alice)  { Fabricate(:account, username: 'alice') }
-  let(:bob)    { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') }
-  let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') }
 
-  subject { ReblogService.new }
+  context 'OStatus' do
+    let(:bob)    { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') }
+    let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') }
 
-  before do
-    stub_request(:post, 'http://salmon.example.com')
+    subject { ReblogService.new }
 
-    subject.(alice, status)
-  end
+    before do
+      stub_request(:post, 'http://salmon.example.com')
+      subject.call(alice, status)
+    end
+
+    it 'creates a reblog' do
+      expect(status.reblogs.count).to eq 1
+    end
 
-  it 'creates a reblog' do
-    expect(status.reblogs.count).to eq 1
+    it 'sends a Salmon slap for a remote reblog' do
+      expect(a_request(:post, 'http://salmon.example.com')).to have_been_made
+    end
   end
 
-  it 'sends a Salmon slap for a remote reblog' do
-    expect(a_request(:post, 'http://salmon.example.com')).to have_been_made
+  context 'ActivityPub' do
+    let(:bob)    { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+    let(:status) { Fabricate(:status, account: bob) }
+
+    subject { ReblogService.new }
+
+    before do
+      stub_request(:post, bob.inbox_url)
+      allow(ActivityPub::DistributionWorker).to receive(:perform_async)
+      subject.call(alice, status)
+    end
+
+    it 'creates a reblog' do
+      expect(status.reblogs.count).to eq 1
+    end
+
+    it 'distributes to followers' do
+      expect(ActivityPub::DistributionWorker).to have_received(:perform_async)
+    end
+
+    it 'sends an announce activity to the author' do
+      expect(a_request(:post, bob.inbox_url)).to have_been_made.once
+    end
   end
 end
diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb
index 50749b633..bf49dd2c9 100644
--- a/spec/services/reject_follow_service_spec.rb
+++ b/spec/services/reject_follow_service_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe RejectFollowService do
     end
   end
 
-  describe 'remote' do
+  describe 'remote OStatus' do
     let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
 
     before do
@@ -42,8 +42,30 @@ RSpec.describe RejectFollowService do
     it 'sends a follow request rejection salmon slap' do
       expect(a_request(:post, "http://salmon.example.com/").with { |req|
         xml = OStatus2::Salmon.new.unpack(req.body)
-        xml.match(TagManager::VERBS[:reject])
+        xml.match(OStatus::TagManager::VERBS[:reject])
       }).to have_been_made.once
     end
   end
+
+  describe 'remote ActivityPub' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
+
+    before do
+      FollowRequest.create(account: bob, target_account: sender)
+      stub_request(:post, bob.inbox_url).to_return(status: 200)
+      subject.call(bob, sender)
+    end
+
+    it 'removes follow request' do
+      expect(bob.requested?(sender)).to be false
+    end
+
+    it 'does not create follow relation' do
+      expect(bob.following?(sender)).to be false
+    end
+
+    it 'sends a reject activity' do
+      expect(a_request(:post, bob.inbox_url)).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb
index a3bce7613..b60015928 100644
--- a/spec/services/remove_status_service_spec.rb
+++ b/spec/services/remove_status_service_spec.rb
@@ -6,14 +6,21 @@ RSpec.describe RemoveStatusService do
   let!(:alice)  { Fabricate(:account) }
   let!(:bob)    { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
   let!(:jeff)   { Fabricate(:account) }
+  let!(:hank)   { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+  let!(:bill)   { Fabricate(:account, username: 'bill', protocol: :activitypub, domain: 'example2.com', inbox_url: 'http://example2.com/inbox') }
 
   before do
     stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {})
     stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {})
+    stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+    stub_request(:post, 'http://example2.com/inbox').to_return(status: 200)
 
     Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now)
     jeff.follow!(alice)
+    hank.follow!(alice)
+
     @status = PostStatusService.new.call(alice, 'Hello @bob@example.com')
+    Fabricate(:status, account: bill, reblog: @status, uri: 'hoge')
     subject.call(@status)
   end
 
@@ -27,14 +34,22 @@ RSpec.describe RemoveStatusService do
 
   it 'sends PuSH update to PuSH subscribers' do
     expect(a_request(:post, 'http://example.com/push').with { |req|
-      req.body.match(TagManager::VERBS[:delete])
+      req.body.match(OStatus::TagManager::VERBS[:delete])
     }).to have_been_made
   end
 
+  it 'sends delete activity to followers' do
+    expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice
+  end
+
   it 'sends Salmon slap to previously mentioned users' do
     expect(a_request(:post, "http://example.com/salmon").with { |req|
       xml = OStatus2::Salmon.new.unpack(req.body)
-      xml.match(TagManager::VERBS[:delete])
+      xml.match(OStatus::TagManager::VERBS[:delete])
     }).to have_been_made.once
   end
+
+  it 'sends delete activity to rebloggers' do
+    expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made
+  end
 end
diff --git a/spec/services/resolve_remote_account_service_spec.rb b/spec/services/resolve_remote_account_service_spec.rb
index c3b902b34..d0bb6a137 100644
--- a/spec/services/resolve_remote_account_service_spec.rb
+++ b/spec/services/resolve_remote_account_service_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe ResolveRemoteAccountService do
-  subject { ResolveRemoteAccountService.new }
+  subject { described_class.new }
 
   before do
     stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
@@ -29,44 +29,83 @@ RSpec.describe ResolveRemoteAccountService do
     expect(subject.call('catsrgr8@example.com')).to be_nil
   end
 
-  it 'returns an already existing remote account' do
-    old_account      = Fabricate(:account, username: 'gargron', domain: 'quitter.no')
-    returned_account = subject.call('gargron@quitter.no')
+  it 'prevents hijacking existing accounts' do
+    account = subject.call('hacker1@redirected.com')
+    expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477'
+  end
 
-    expect(old_account.id).to eq returned_account.id
+  it 'prevents hijacking inexisting accounts' do
+    expect(subject.call('hacker2@redirected.com')).to be_nil
   end
 
-  it 'returns a new remote account' do
-    account = subject.call('gargron@quitter.no')
+  context 'with an OStatus account' do
+    it 'returns an already existing remote account' do
+      old_account      = Fabricate(:account, username: 'gargron', domain: 'quitter.no')
+      returned_account = subject.call('gargron@quitter.no')
 
-    expect(account.username).to eq 'gargron'
-    expect(account.domain).to eq 'quitter.no'
-    expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
-  end
+      expect(old_account.id).to eq returned_account.id
+    end
 
-  it 'follows a legitimate account redirection' do
-    account = subject.call('gargron@redirected.com')
+    it 'returns a new remote account' do
+      account = subject.call('gargron@quitter.no')
 
-    expect(account.username).to eq 'gargron'
-    expect(account.domain).to eq 'quitter.no'
-    expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
-  end
+      expect(account.username).to eq 'gargron'
+      expect(account.domain).to eq 'quitter.no'
+      expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
+    end
 
-  it 'prevents hijacking existing accounts' do
-    account = subject.call('hacker1@redirected.com')
-    expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477'
-  end
+    it 'follows a legitimate account redirection' do
+      account = subject.call('gargron@redirected.com')
 
-  it 'prevents hijacking inexisting accounts' do
-    expect(subject.call('hacker2@redirected.com')).to be_nil
+      expect(account.username).to eq 'gargron'
+      expect(account.domain).to eq 'quitter.no'
+      expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
+    end
+
+    it 'returns a new remote account' do
+      account = subject.call('foo@localdomain.com')
+
+      expect(account.username).to eq 'foo'
+      expect(account.domain).to eq 'localdomain.com'
+      expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom'
+    end
   end
 
-  it 'returns a new remote account' do
-    account = subject.call('foo@localdomain.com')
+  context 'with an ActivityPub account' do
+    before do
+      stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt'))
+      stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt'))
+      stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt'))
+      stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404)
+    end
+
+    it 'fallback to OStatus if actor json could not be fetched' do
+      stub_request(:get, "https://ap.example.com/users/foo").to_return(status: 404)
+
+      account = subject.call('foo@ap.example.com')
+
+      expect(account.ostatus?).to eq true
+      expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom'
+    end
+
+    it 'fallback to OStatus if actor json did not have inbox_url' do
+      stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-noinbox.txt'))
+
+      account = subject.call('foo@ap.example.com')
+
+      expect(account.ostatus?).to eq true
+      expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom'
+    end
+
+    it 'returns new remote account' do
+      account = subject.call('foo@ap.example.com')
+
+      expect(account.activitypub?).to eq true
+      expect(account.domain).to eq 'ap.example.com'
+      expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
+    end
 
-    expect(account.username).to eq 'foo'
-    expect(account.domain).to eq 'localdomain.com'
-    expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom'
+    pending
   end
 
   it 'processes one remote account at a time using locks' do
@@ -78,7 +117,7 @@ RSpec.describe ResolveRemoteAccountService do
       Thread.new do
         true while wait_for_start
         begin
-          return_values << ResolveRemoteAccountService.new.call('foo@localdomain.com')
+          return_values << described_class.new.call('foo@localdomain.com')
         rescue ActiveRecord::RecordNotUnique
           fail_occurred = true
         end
diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb
index 1b9ae1239..ca7a6b77e 100644
--- a/spec/services/unblock_service_spec.rb
+++ b/spec/services/unblock_service_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe UnblockService do
     end
   end
 
-  describe 'remote' do
+  describe 'remote OStatus' do
     let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
 
     before do
@@ -28,14 +28,32 @@ RSpec.describe UnblockService do
     end
 
     it 'destroys the blocking relation' do
-      expect(sender.following?(bob)).to be false
+      expect(sender.blocking?(bob)).to be false
     end
 
     it 'sends an unblock salmon slap' do
       expect(a_request(:post, "http://salmon.example.com/").with { |req|
         xml = OStatus2::Salmon.new.unpack(req.body)
-        xml.match(TagManager::VERBS[:unblock])
+        xml.match(OStatus::TagManager::VERBS[:unblock])
       }).to have_been_made.once
     end
   end
+
+  describe 'remote ActivityPub' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
+
+    before do
+      sender.block!(bob)
+      stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+      subject.call(sender, bob)
+    end
+
+    it 'destroys the blocking relation' do
+      expect(sender.blocking?(bob)).to be false
+    end
+
+    it 'sends an unblock activity' do
+      expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb
index 8ec2148a1..021e76782 100644
--- a/spec/services/unfollow_service_spec.rb
+++ b/spec/services/unfollow_service_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe UnfollowService do
     end
   end
 
-  describe 'remote' do
-    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+  describe 'remote OStatus' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
 
     before do
       sender.follow!(bob)
@@ -34,8 +34,26 @@ RSpec.describe UnfollowService do
     it 'sends an unfollow salmon slap' do
       expect(a_request(:post, "http://salmon.example.com/").with { |req|
         xml = OStatus2::Salmon.new.unpack(req.body)
-        xml.match(TagManager::VERBS[:unfollow])
+        xml.match(OStatus::TagManager::VERBS[:unfollow])
       }).to have_been_made.once
     end
   end
+
+  describe 'remote ActivityPub' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
+
+    before do
+      sender.follow!(bob)
+      stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+      subject.call(sender, bob)
+    end
+
+    it 'destroys the following relation' do
+      expect(sender.following?(bob)).to be false
+    end
+
+    it 'sends an unfollow activity' do
+      expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/unsubscribe_service_spec.rb b/spec/services/unsubscribe_service_spec.rb
index c81772037..2a02f4c75 100644
--- a/spec/services/unsubscribe_service_spec.rb
+++ b/spec/services/unsubscribe_service_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe UnsubscribeService do
     stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error)
     subject.call(account)
 
-    expect(logger).to have_received(:debug).with(/PuSH subscription request for bob@example.com could not be made due to HTTP or SSL error/)
+    expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/)
   end
 
   def stub_logger
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 2bc462121..eecaec4ac 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,11 +1,15 @@
 require 'simplecov'
 
+GC.disable
+
 SimpleCov.start 'rails' do
   add_group 'Services', 'app/services'
   add_group 'Presenters', 'app/presenters'
   add_group 'Validators', 'app/validators'
 end
 
+gc_counter = -1
+
 RSpec.configure do |config|
   config.expect_with :rspec do |expectations|
     expectations.include_chain_clauses_in_custom_matcher_descriptions = true
@@ -22,8 +26,21 @@ RSpec.configure do |config|
   end
 
   config.after :suite do
+    gc_counter = 0
     FileUtils.rm_rf(Dir["#{Rails.root}/spec/test_files/"])
   end
+
+  config.after :each do
+    gc_counter += 1
+
+    if gc_counter > 19
+      GC.enable
+      GC.start
+      GC.disable
+
+      gc_counter = 0
+    end
+  end
 end
 
 def body_as_json
diff --git a/spec/validators/status_length_validator_spec.rb b/spec/validators/status_length_validator_spec.rb
index e2d1a15ec..9355c7e3f 100644
--- a/spec/validators/status_length_validator_spec.rb
+++ b/spec/validators/status_length_validator_spec.rb
@@ -7,26 +7,31 @@ describe StatusLengthValidator do
     it 'does not add errors onto remote statuses'
     it 'does not add errors onto local reblogs'
 
-    it 'adds an error when content warning is over 500 characters' do
-      status = double(spoiler_text: 'a' * 520, text: '', errors: double(add: nil), local?: true, reblog?: false)
+    it 'adds an error when content warning is over MAX_CHARS characters' do
+      chars = StatusLengthValidator::MAX_CHARS + 1
+      status = double(spoiler_text: 'a' * chars, text: '', errors: double(add: nil), local?: true, reblog?: false)
       subject.validate(status)
       expect(status.errors).to have_received(:add)
     end
 
-    it 'adds an error when text is over 500 characters' do
-      status = double(spoiler_text: '', text: 'a' * 520, errors: double(add: nil), local?: true, reblog?: false)
+    it 'adds an error when text is over MAX_CHARS characters' do
+      chars = StatusLengthValidator::MAX_CHARS + 1
+      status = double(spoiler_text: '', text: 'a' * chars, errors: double(add: nil), local?: true, reblog?: false)
       subject.validate(status)
       expect(status.errors).to have_received(:add)
     end
 
-    it 'adds an error when text and content warning are over 500 characters total' do
-      status = double(spoiler_text: 'a' * 250, text: 'b' * 251, errors: double(add: nil), local?: true, reblog?: false)
+    it 'adds an error when text and content warning are over MAX_CHARS characters total' do
+      chars1 = 20
+      chars2 = StatusLengthValidator::MAX_CHARS + 1 - chars1
+      status = double(spoiler_text: 'a' * chars1, text: 'b' * chars2, errors: double(add: nil), local?: true, reblog?: false)
       subject.validate(status)
       expect(status.errors).to have_received(:add)
     end
 
     it 'counts URLs as 23 characters flat' do
-      text   = ('a' * 476) + " http://#{'b' * 30}.com/example"
+      chars = StatusLengthValidator::MAX_CHARS - 1 - 23
+      text   = ('a' * chars) + " http://#{'b' * 30}.com/example"
       status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false)
 
       subject.validate(status)
@@ -34,7 +39,9 @@ describe StatusLengthValidator do
     end
 
     it 'counts only the front part of remote usernames' do
-      text   = ('a' * 475) + " @alice@#{'b' * 30}.com"
+      username = '@alice'
+      chars = StatusLengthValidator::MAX_CHARS - 1 - username.length
+      text   = ('a' * 475) + " #{username}@#{'b' * 30}.com"
       status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false)
 
       subject.validate(status)
diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb
index d460adfe5..b2f2658de 100644
--- a/spec/views/about/show.html.haml_spec.rb
+++ b/spec/views/about/show.html.haml_spec.rb
@@ -15,7 +15,9 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
                                 site_title: 'something',
                                 site_description: 'something',
                                 version_number: '1.0',
+                                source_url: 'https://github.com/tootsuite/mastodon',
                                 open_registrations: false,
+                                thumbnail: nil,
                                 closed_registrations_message: 'yes',
                                 commit_hash: commit_hash)
 
diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb
index 6cc3b117a..59ea40990 100644
--- a/spec/views/stream_entries/show.html.haml_spec.rb
+++ b/spec/views/stream_entries/show.html.haml_spec.rb
@@ -80,9 +80,9 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
 
     header_tags = view.content_for(:header_tags)
 
-    expect(header_tags).to match(%r{<meta content='.+' property='og:title'>})
-    expect(header_tags).to match(%r{<meta content='article' property='og:type'>})
-    expect(header_tags).to match(%r{<meta content='.+' property='og:image'>})
-    expect(header_tags).to match(%r{<meta content='http://.+' property='og:url'>})
+    expect(header_tags).to match(%r{<meta content=".+" property="og:title" />})
+    expect(header_tags).to match(%r{<meta content="article" property="og:type" />})
+    expect(header_tags).to match(%r{<meta content=".+" property="og:image" />})
+    expect(header_tags).to match(%r{<meta content="http://.+" property="og:url" />})
   end
 end
diff --git a/spec/workers/activitypub/delivery_worker_spec.rb b/spec/workers/activitypub/delivery_worker_spec.rb
new file mode 100644
index 000000000..351be185c
--- /dev/null
+++ b/spec/workers/activitypub/delivery_worker_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ActivityPub::DeliveryWorker do
+  subject { described_class.new }
+
+  let(:sender)  { Fabricate(:account) }
+  let(:payload) { 'test' }
+
+  describe 'perform' do
+    it 'performs a request' do
+      stub_request(:post, 'https://example.com/api').to_return(status: 200)
+      subject.perform(payload, sender.id, 'https://example.com/api')
+      expect(a_request(:post, 'https://example.com/api')).to have_been_made.once
+    end
+
+    it 'raises when request fails' do
+      stub_request(:post, 'https://example.com/api').to_return(status: 500)
+      expect { subject.perform(payload, sender.id, 'https://example.com/api') }.to raise_error Mastodon::UnexpectedResponseError
+    end
+  end
+end
diff --git a/spec/workers/activitypub/distribution_worker_spec.rb b/spec/workers/activitypub/distribution_worker_spec.rb
new file mode 100644
index 000000000..368ca025a
--- /dev/null
+++ b/spec/workers/activitypub/distribution_worker_spec.rb
@@ -0,0 +1,48 @@
+require 'rails_helper'
+
+describe ActivityPub::DistributionWorker do
+  subject { described_class.new }
+
+  let(:status)   { Fabricate(:status) }
+  let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') }
+
+  describe '#perform' do
+    before do
+      allow(ActivityPub::DeliveryWorker).to receive(:push_bulk)
+      follower.follow!(status.account)
+    end
+
+    context 'with public status' do
+      before do
+        status.update(visibility: :public)
+      end
+
+      it 'delivers to followers' do
+        subject.perform(status.id)
+        expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com'])
+      end
+    end
+
+    context 'with private status' do
+      before do
+        status.update(visibility: :private)
+      end
+
+      it 'delivers to followers' do
+        subject.perform(status.id)
+        expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com'])
+      end
+    end
+
+    context 'with direct status' do
+      before do
+        status.update(visibility: :direct)
+      end
+
+      it 'does nothing' do
+        subject.perform(status.id)
+        expect(ActivityPub::DeliveryWorker).to_not have_received(:push_bulk)
+      end
+    end
+  end
+end
diff --git a/spec/workers/activitypub/processing_worker_spec.rb b/spec/workers/activitypub/processing_worker_spec.rb
new file mode 100644
index 000000000..b42c0bdbc
--- /dev/null
+++ b/spec/workers/activitypub/processing_worker_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+describe ActivityPub::ProcessingWorker do
+  subject { described_class.new }
+
+  let(:account) { Fabricate(:account) }
+
+  describe '#perform' do
+    it 'delegates to ActivityPub::ProcessCollectionService' do
+      allow(ActivityPub::ProcessCollectionService).to receive(:new).and_return(double(:service, call: nil))
+      subject.perform(account.id, '')
+      expect(ActivityPub::ProcessCollectionService).to have_received(:new)
+    end
+  end
+end
diff --git a/spec/workers/activitypub/update_distribution_worker_spec.rb b/spec/workers/activitypub/update_distribution_worker_spec.rb
new file mode 100644
index 000000000..688a424d5
--- /dev/null
+++ b/spec/workers/activitypub/update_distribution_worker_spec.rb
@@ -0,0 +1,20 @@
+require 'rails_helper'
+
+describe ActivityPub::UpdateDistributionWorker do
+  subject { described_class.new }
+
+  let(:account)  { Fabricate(:account) }
+  let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') }
+
+  describe '#perform' do
+    before do
+      allow(ActivityPub::DeliveryWorker).to receive(:push_bulk)
+      follower.follow!(account)
+    end
+
+    it 'delivers to followers' do
+      subject.perform(account.id)
+      expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com'])
+    end
+  end
+end
diff --git a/spec/workers/pubsubhubbub/distribution_worker_spec.rb b/spec/workers/pubsubhubbub/distribution_worker_spec.rb
index 89191c084..584485079 100644
--- a/spec/workers/pubsubhubbub/distribution_worker_spec.rb
+++ b/spec/workers/pubsubhubbub/distribution_worker_spec.rb
@@ -18,28 +18,29 @@ describe Pubsubhubbub::DistributionWorker do
     it 'delivers payload to all subscriptions' do
       allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk)
       subject.perform(status.stream_entry.id)
-      expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([anonymous_subscription, subscription_with_follower])
+      expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([anonymous_subscription.id, subscription_with_follower.id])
     end
   end
 
-  describe 'with private status' do
-    let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :private) }
+  context 'when OStatus privacy is not used' do
+    describe 'with private status' do
+      let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :private) }
 
-    it 'delivers payload only to subscriptions with followers' do
-      allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk)
-      subject.perform(status.stream_entry.id)
-      expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([subscription_with_follower])
-      expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk).with([anonymous_subscription])
+      it 'does not deliver anything' do
+        allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk)
+        subject.perform(status.stream_entry.id)
+        expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk)
+      end
     end
-  end
 
-  describe 'with direct status' do
-    let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :direct) }
+    describe 'with direct status' do
+      let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :direct) }
 
-    it 'does not deliver payload' do
-      allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk)
-      subject.perform(status.stream_entry.id)
-      expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk)
+      it 'does not deliver payload' do
+        allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk)
+        subject.perform(status.stream_entry.id)
+        expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk)
+      end
     end
   end
 end