about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
authorbeatrix <beatrix.bitrot@gmail.com>2017-09-28 21:48:28 -0400
committerGitHub <noreply@github.com>2017-09-28 21:48:28 -0400
commitc027a7bd4d7b5af21f4b201d656f7251fa3606a1 (patch)
treece2c2327b26358c26cb899ea918988af373ca6d6 /spec
parent210e6776fce016666ecfd248b2208c487f3440f9 (diff)
parent53f829dfa8bc376041a442dc84c22aa1cbfcb9d0 (diff)
Merge pull request #157 from glitch-soc/merging-upstream
ABRACA-HRRRRRRRRRRRNGGGGGGGHHH!!!!!!!!!!!!!!!!!!!
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/api/v1/accounts/relationships_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/custom_emojis_controller_spec.rb18
-rw-r--r--spec/controllers/api/v1/media_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/statuses/favourites_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/statuses/pins_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/statuses/reblogs_controller_spec.rb2
-rw-r--r--spec/controllers/home_controller_spec.rb2
-rw-r--r--spec/fabricators/custom_emoji_fabricator.rb5
-rw-r--r--spec/fabricators/site_upload_fabricator.rb3
-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/javascript/components/dropdown_menu.test.js132
-rw-r--r--spec/javascript/components/emojify.test.js16
-rw-r--r--spec/lib/activitypub/activity/create_spec.rb129
-rw-r--r--spec/lib/activitypub/tag_manager_spec.rb2
-rw-r--r--spec/lib/emoji_spec.rb15
-rw-r--r--spec/lib/formatter_spec.rb126
-rw-r--r--spec/lib/language_detector_spec.rb34
-rw-r--r--spec/lib/ostatus/atom_serializer_spec.rb104
-rw-r--r--spec/lib/ostatus/tag_manager_spec.rb70
-rw-r--r--spec/lib/tag_manager_spec.rb75
-rw-r--r--spec/models/custom_emoji_spec.rb25
-rw-r--r--spec/models/site_upload_spec.rb5
-rw-r--r--spec/models/status_spec.rb29
-rw-r--r--spec/services/activitypub/process_collection_service_spec.rb4
-rw-r--r--spec/services/authorize_follow_service_spec.rb2
-rw-r--r--spec/services/batched_remove_status_service_spec.rb4
-rw-r--r--spec/services/block_service_spec.rb2
-rw-r--r--spec/services/favourite_service_spec.rb2
-rw-r--r--spec/services/fetch_link_card_service_spec.rb11
-rw-r--r--spec/services/follow_service_spec.rb4
-rw-r--r--spec/services/post_status_service_spec.rb9
-rw-r--r--spec/services/reject_follow_service_spec.rb2
-rw-r--r--spec/services/remove_status_service_spec.rb4
-rw-r--r--spec/services/resolve_remote_account_service_spec.rb33
-rw-r--r--spec/services/unblock_service_spec.rb2
-rw-r--r--spec/services/unfollow_service_spec.rb2
-rw-r--r--spec/views/about/show.html.haml_spec.rb1
-rw-r--r--spec/views/stream_entries/show.html.haml_spec.rb8
-rw-r--r--spec/workers/pubsubhubbub/distribution_worker_spec.rb39
44 files changed, 639 insertions, 388 deletions
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
index a9073b197..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 eq 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 eq 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/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/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/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
index 2e170da24..79005c9de 100644
--- a/spec/controllers/api/v1/statuses/pins_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
@@ -32,7 +32,7 @@ describe Api::V1::Statuses::PinsController 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[:pinned]).to be true
       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/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/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/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/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/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index fcb044ebc..cdd499150 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe ActivityPub::Activity::Create do
 
   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
@@ -170,6 +171,26 @@ RSpec.describe ActivityPub::Activity::Create do
       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
         {
@@ -194,6 +215,27 @@ RSpec.describe ActivityPub::Activity::Create do
       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
         {
@@ -217,5 +259,92 @@ RSpec.describe ActivityPub::Activity::Create do
         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/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb
index dea8abc65..0d1665216 100644
--- a/spec/lib/activitypub/tag_manager_spec.rb
+++ b/spec/lib/activitypub/tag_manager_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe ActivityPub::TagManager do
 
     it 'returns the local status for OStatus tag: URI' do
       status = Fabricate(:status)
-      expect(subject.uri_to_resource(::TagManager.instance.uri_for(status), Status)).to eq 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
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 ab04ccbab..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,6 +223,45 @@ 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
@@ -183,6 +270,45 @@ RSpec.describe Formatter do
       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
 
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 0451eceeb..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
@@ -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
@@ -494,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
@@ -503,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
@@ -727,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
 
@@ -757,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
@@ -766,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
@@ -814,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
 
@@ -844,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
@@ -853,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
@@ -922,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
@@ -993,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
 
@@ -1022,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
@@ -1105,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
@@ -1114,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
@@ -1178,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
 
@@ -1222,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
@@ -1232,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
@@ -1326,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
 
@@ -1347,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
@@ -1356,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
@@ -1395,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
 
@@ -1412,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
@@ -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
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/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index 1cd6e0a6f..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,71 +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 '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
-
   describe '#url_for' do
     let(:alice) { Fabricate(:account, username: 'alice') }
 
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/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_spec.rb b/spec/models/status_spec.rb
index 484effd5e..9cb71d715 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -173,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']) }
 
-      results = described_class.local_only
-      expect(results).to include(local_status)
-      expect(results).not_to include(remote_status)
+      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
+
+      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
 
@@ -529,6 +532,14 @@ RSpec.describe Status, type: :model do
     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')
diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb
index 249b12470..c1cc22523 100644
--- a/spec/services/activitypub/process_collection_service_spec.rb
+++ b/spec/services/activitypub/process_collection_service_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::ProcessCollectionService do
-  let(:actor) { Fabricate(:account) }
+  let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
 
   let(:payload) do
     {
@@ -24,7 +24,7 @@ RSpec.describe ActivityPub::ProcessCollectionService do
   describe '#call' do
     context 'when actor is the sender'
     context 'when actor differs from sender' do
-      let(:forwarder) { Fabricate(:account) }
+      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!)
diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb
index d74eb41a2..6ea4d83da 100644
--- a/spec/services/authorize_follow_service_spec.rb
+++ b/spec/services/authorize_follow_service_spec.rb
@@ -42,7 +42,7 @@ 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
diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb
index b1e9ac567..f5c9adfb5 100644
--- a/spec/services/batched_remove_status_service_spec.rb
+++ b/spec/services/batched_remove_status_service_spec.rb
@@ -50,14 +50,14 @@ RSpec.describe BatchedRemoveStatusService do
 
   it 'sends PuSH update to PuSH subscribers' do
     expect(a_request(:post, 'http://example.com/push').with { |req|
-      matches = req.body.match(TagManager::VERBS[:delete])
+      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
 
diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb
index bd2ab3d53..c69ff7804 100644
--- a/spec/services/block_service_spec.rb
+++ b/spec/services/block_service_spec.rb
@@ -32,7 +32,7 @@ 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
diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb
index 2ab1f32ca..5bf2c74a9 100644
--- a/spec/services/favourite_service_spec.rb
+++ b/spec/services/favourite_service_spec.rb
@@ -34,7 +34,7 @@ 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
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index b0aa740ac..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)
@@ -52,6 +54,15 @@ RSpec.describe FetchLinkCardService do
         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
diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb
index 1e2378031..ceb39e5e6 100644
--- a/spec/services/follow_service_spec.rb
+++ b/spec/services/follow_service_spec.rb
@@ -60,7 +60,7 @@ 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
@@ -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
 
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 4182c4e1f..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
diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb
index 2e06345b3..bf49dd2c9 100644
--- a/spec/services/reject_follow_service_spec.rb
+++ b/spec/services/reject_follow_service_spec.rb
@@ -42,7 +42,7 @@ 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
diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb
index 8b34bdb6b..b60015928 100644
--- a/spec/services/remove_status_service_spec.rb
+++ b/spec/services/remove_status_service_spec.rb
@@ -34,7 +34,7 @@ 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
 
@@ -45,7 +45,7 @@ RSpec.describe RemoveStatusService do
   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
 
diff --git a/spec/services/resolve_remote_account_service_spec.rb b/spec/services/resolve_remote_account_service_spec.rb
index d0eab2310..d0bb6a137 100644
--- a/spec/services/resolve_remote_account_service_spec.rb
+++ b/spec/services/resolve_remote_account_service_spec.rb
@@ -72,6 +72,39 @@ RSpec.describe ResolveRemoteAccountService do
   end
 
   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
+
     pending
   end
 
diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb
index def4981e7..ca7a6b77e 100644
--- a/spec/services/unblock_service_spec.rb
+++ b/spec/services/unblock_service_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe UnblockService do
     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
diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb
index 29040431e..021e76782 100644
--- a/spec/services/unfollow_service_spec.rb
+++ b/spec/services/unfollow_service_spec.rb
@@ -34,7 +34,7 @@ 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
diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb
index 95a8a6323..b2f2658de 100644
--- a/spec/views/about/show.html.haml_spec.rb
+++ b/spec/views/about/show.html.haml_spec.rb
@@ -17,6 +17,7 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
                                 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/pubsubhubbub/distribution_worker_spec.rb b/spec/workers/pubsubhubbub/distribution_worker_spec.rb
index 5c22e7fa8..584485079 100644
--- a/spec/workers/pubsubhubbub/distribution_worker_spec.rb
+++ b/spec/workers/pubsubhubbub/distribution_worker_spec.rb
@@ -18,48 +18,11 @@ 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])
-    end
-  end
-
-  context 'when OStatus privacy is used' do
-    around do |example|
-      before_val = Rails.configuration.x.use_ostatus_privacy
-      Rails.configuration.x.use_ostatus_privacy = true
-      example.run
-      Rails.configuration.x.use_ostatus_privacy = before_val
-    end
-
-    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])
-      end
-    end
-
-    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)
-      end
+      expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([anonymous_subscription.id, subscription_with_follower.id])
     end
   end
 
   context 'when OStatus privacy is not used' do
-    around do |example|
-      before_val = Rails.configuration.x.use_ostatus_privacy
-      Rails.configuration.x.use_ostatus_privacy = false
-      example.run
-      Rails.configuration.x.use_ostatus_privacy = before_val
-    end
-
     describe 'with private status' do
       let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :private) }