about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
authorReverite <github@reverite.sh>2019-02-24 22:18:10 -0800
committerReverite <github@reverite.sh>2019-02-24 22:18:10 -0800
commit54e480ca0939ba737f5abdf4ee861cd63c025865 (patch)
tree760cdd75fd0922266b60e784c06db99902ef5692 /spec
parentff9a09a9a7f73b558c53f334573b94198eb8d08a (diff)
parentd82de360c13894746d3974d11c9505c8937ebdee (diff)
Merge branch 'glitch' into production
Diffstat (limited to 'spec')
-rw-r--r--spec/fabricators/featured_tag_fabricator.rb6
-rw-r--r--spec/lib/activitypub/activity/announce_spec.rb158
-rw-r--r--spec/lib/activitypub/activity/create_spec.rb710
-rw-r--r--spec/lib/formatter_spec.rb84
-rw-r--r--spec/models/concerns/account_interactions_spec.rb4
-rw-r--r--spec/models/featured_tag_spec.rb4
-rw-r--r--spec/validators/email_mx_validator_spec.rb38
7 files changed, 696 insertions, 308 deletions
diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb
new file mode 100644
index 000000000..25cbdaac0
--- /dev/null
+++ b/spec/fabricators/featured_tag_fabricator.rb
@@ -0,0 +1,6 @@
+Fabricator(:featured_tag) do
+  account
+  tag
+  statuses_count 1_337
+  last_status_at Time.now.utc
+end
diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb
index 54dd52a60..aa58d9e23 100644
--- a/spec/lib/activitypub/activity/announce_spec.rb
+++ b/spec/lib/activitypub/activity/announce_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::Activity::Announce do
-  let(:sender)    { Fabricate(:account) }
+  let(:sender)    { Fabricate(:account, followers_url: 'http://example.com/followers', uri: 'https://example.com/actor') }
   let(:recipient) { Fabricate(:account) }
   let(:status)    { Fabricate(:status, account: recipient) }
 
@@ -10,20 +10,162 @@ RSpec.describe ActivityPub::Activity::Announce 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),
+      actor: 'https://example.com/actor',
+      object: object_json,
     }.with_indifferent_access
   end
 
+  let(:unknown_object_json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'https://example.com/actor/hello-world',
+      type: 'Note',
+      attributedTo: 'https://example.com/actor',
+      content: 'Hello world',
+      to: 'http://example.com/followers',
+    }
+  end
+
+  subject { described_class.new(json, sender) }
+
   describe '#perform' do
-    subject { described_class.new(json, sender) }
+    context 'when sender is followed by a local account' do
+      before do
+        Fabricate(:account).follow!(sender)
+        stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
+        subject.perform
+      end
+
+      context 'a known status' do
+        let(:object_json) do
+          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
+
+      context 'an unknown status' do
+        let(:object_json) { 'https://example.com/actor/hello-world' }
+
+        it 'creates a reblog by sender of status' do
+          reblog = sender.statuses.first
+
+          expect(reblog).to_not be_nil
+          expect(reblog.reblog.text).to eq 'Hello world'
+        end
+      end
+
+      context 'self-boost of a previously unknown status with missing attributedTo' do
+        let(:object_json) do
+          {
+            id: 'https://example.com/actor#bar',
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'creates a reblog by sender of status' do
+          expect(sender.reblogged?(sender.statuses.first)).to be true
+        end
+      end
+
+      context 'self-boost of a previously unknown status with correct attributedTo' do
+        let(:object_json) do
+          {
+            id: 'https://example.com/actor#bar',
+            type: 'Note',
+            content: 'Lorem ipsum',
+            attributedTo: 'https://example.com/actor',
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'creates a reblog by sender of status' do
+          expect(sender.reblogged?(sender.statuses.first)).to be true
+        end
+      end
+    end
+
+    context 'when the status belongs to a local user' do
+      before do
+        subject.perform
+      end
+
+      let(:object_json) do
+        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
+
+    context 'when the sender is relayed' do
+      let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') }
+      let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') }
+
+      subject { described_class.new(json, sender, relayed_through_account: relay_account) }
 
-    before do
-      subject.perform
+      context 'and the relay is enabled' do
+        before do
+          relay.update(state: :accepted)
+          subject.perform
+        end
+
+        let(:object_json) do
+          {
+            id: 'https://example.com/actor#bar',
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'creates a reblog by sender of status' do
+          expect(sender.statuses.count).to eq 2
+        end
+      end
+
+      context 'and the relay is disabled' do
+        before do
+          subject.perform
+        end
+
+        let(:object_json) do
+          {
+            id: 'https://example.com/actor#bar',
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'does not create anything' do
+          expect(sender.statuses.count).to eq 0
+        end
+      end
     end
 
-    it 'creates a reblog by sender of status' do
-      expect(sender.reblogged?(status)).to be true
+    context 'when the sender has no relevance to local activity' do
+      before do
+        subject.perform
+      end
+
+      let(:object_json) do
+        {
+          id: 'https://example.com/actor#bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          to: 'http://example.com/followers',
+        }
+      end
+
+      it 'does not create anything' do
+        expect(sender.statuses.count).to eq 0
+      end
     end
   end
 end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index cd20b7c7c..26cb84871 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -13,8 +13,6 @@ RSpec.describe ActivityPub::Activity::Create do
     }.with_indifferent_access
   end
 
-  subject { described_class.new(json, sender) }
-
   before do
     sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
 
@@ -23,59 +21,407 @@ RSpec.describe ActivityPub::Activity::Create do
   end
 
   describe '#perform' do
-    before do
-      subject.perform
-    end
-
-    context 'standalone' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          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
+    context 'when fetching' do
+      subject { described_class.new(json, sender) }
+
+      before do
+        subject.perform
+      end
+
+      context 'standalone' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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 'limited' do
+        let(:recipient) { Fabricate(:account) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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 'limited'
+        end
+
+        it 'creates silent mention' do
+          status = sender.statuses.first
+          expect(status.mentions.first).to be_silent
+        end
+      end
+
+      context 'direct' do
+        let(:recipient) { Fabricate(:account) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: ActivityPub::TagManager.instance.uri_for(recipient),
+            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.visibility).to eq 'direct'
+        end
+      end
+
+      context 'as a reply' do
+        let(:original_status) { Fabricate(:status) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            attachment: [
+              {
+                type: 'Document',
+                mediaType: '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 with focal points' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            attachment: [
+              {
+                type: 'Document',
+                mediaType: 'image/png',
+                url: 'http://example.com/attachment.png',
+                focalPoint: [0.5, -0.7],
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7')
+        end
+      end
+
+      context 'with media attachments missing url' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            attachment: [
+              {
+                type: 'Document',
+                mediaType: '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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum :tinking:',
+            tag: [
+              {
+                type: 'Emoji',
+                icon: {
+                  url: '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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum :tinking:',
+            tag: [
+              {
+                type: 'Emoji',
+                icon: {
+                  url: '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 icon' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum :tinking:',
+            tag: [
+              {
+                type: 'Emoji',
+                name: 'tinking',
+              },
+            ],
+          }
+        end
 
-        expect(status).to_not be_nil
-        expect(status.visibility).to eq 'direct'
+        it 'creates status' do
+          status = sender.statuses.first
+          expect(status).to_not be_nil
+        end
       end
     end
 
-    context 'public' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          to: 'https://www.w3.org/ns/activitystreams#Public',
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
+    context 'when sender is followed by local users' do
+      subject { described_class.new(json, sender, delivery: true) }
 
-        expect(status).to_not be_nil
-        expect(status.visibility).to eq 'public'
+      before do
+        Fabricate(:account).follow!(sender)
+        subject.perform
       end
-    end
 
-    context 'unlisted' do
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          cc: 'https://www.w3.org/ns/activitystreams#Public',
         }
       end
 
@@ -83,66 +429,25 @@ RSpec.describe ActivityPub::Activity::Create 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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          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'
+        expect(status.text).to eq 'Lorem ipsum'
       end
     end
 
-    context 'limited' do
-      let(:recipient) { Fabricate(:account) }
+    context 'when sender replies to local status' do
+      let!(:local_status) { Fabricate(:status) }
 
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          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 'limited'
-      end
+      subject { described_class.new(json, sender, delivery: true) }
 
-      it 'creates silent mention' do
-        status = sender.statuses.first
-        expect(status.mentions.first).to be_silent
+      before do
+        subject.perform
       end
-    end
-
-    context 'direct' do
-      let(:recipient) { Fabricate(:account) }
 
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          to: ActivityPub::TagManager.instance.uri_for(recipient),
-          tag: {
-            type: 'Mention',
-            href: ActivityPub::TagManager.instance.uri_for(recipient),
-          },
+          inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status),
         }
       end
 
@@ -150,47 +455,25 @@ RSpec.describe ActivityPub::Activity::Create do
         status = sender.statuses.first
 
         expect(status).to_not be_nil
-        expect(status.visibility).to eq 'direct'
+        expect(status.text).to eq 'Lorem ipsum'
       end
     end
 
-    context 'as a reply' do
-      let(:original_status) { Fabricate(:status) }
+    context 'when sender targets a local user' do
+      let!(:local_account) { Fabricate(:account) }
 
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
+      subject { described_class.new(json, sender, delivery: true) }
 
-        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
+      before do
+        subject.perform
       end
-    end
-
-    context 'with mentions' do
-      let(:recipient) { Fabricate(:account) }
 
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          tag: [
-            {
-              type: 'Mention',
-              href: ActivityPub::TagManager.instance.uri_for(recipient),
-            },
-          ],
+          to: ActivityPub::TagManager.instance.uri_for(local_account),
         }
       end
 
@@ -198,68 +481,25 @@ RSpec.describe ActivityPub::Activity::Create 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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          tag: [
-            {
-              type: 'Mention',
-            },
-          ],
-        }
-      end
-
-      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 media attachments' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          attachment: [
-            {
-              type: 'Document',
-              mediaType: 'image/png',
-              url: 'http://example.com/attachment.png',
-            },
-          ],
-        }
-      end
+    context 'when sender cc\'s a local user' do
+      let!(:local_account) { Fabricate(:account) }
 
-      it 'creates status' do
-        status = sender.statuses.first
+      subject { described_class.new(json, sender, delivery: true) }
 
-        expect(status).to_not be_nil
-        expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
+      before do
+        subject.perform
       end
-    end
 
-    context 'with media attachments with focal points' do
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          attachment: [
-            {
-              type: 'Document',
-              mediaType: 'image/png',
-              url: 'http://example.com/attachment.png',
-              focalPoint: [0.5, -0.7],
-            },
-          ],
+          cc: ActivityPub::TagManager.instance.uri_for(local_account),
         }
       end
 
@@ -267,143 +507,27 @@ RSpec.describe ActivityPub::Activity::Create do
         status = sender.statuses.first
 
         expect(status).to_not be_nil
-        expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7')
+        expect(status.text).to eq 'Lorem ipsum'
       end
     end
 
-    context 'with media attachments missing url' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          attachment: [
-            {
-              type: 'Document',
-              mediaType: 'image/png',
-            },
-          ],
-        }
-      end
+    context 'when the sender has no relevance to local activity' do
+      subject { described_class.new(json, sender, delivery: true) }
 
-      it 'creates status' do
-        status = sender.statuses.first
-        expect(status).to_not be_nil
+      before do
+        subject.perform
       end
-    end
 
-    context 'with hashtags' do
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum :tinking:',
-          tag: [
-            {
-              type: 'Emoji',
-              icon: {
-                url: '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: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum :tinking:',
-          tag: [
-            {
-              type: 'Emoji',
-              icon: {
-                url: '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 icon' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          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
+      it 'does not create anything' do
+        expect(sender.statuses.count).to eq 0
       end
     end
   end
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
index 0c1efe7c3..96d2fc7e0 100644
--- a/spec/lib/formatter_spec.rb
+++ b/spec/lib/formatter_spec.rb
@@ -74,10 +74,36 @@ RSpec.describe Formatter do
     end
 
     context 'given a URL with a query string' do
-      let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
+      context 'with escaped unicode character' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
 
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;q=autolink"'
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;q=autolink"'
+        end
+      end
+
+      context 'with unicode character' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&amp;q=autolink"'
+        end
+      end
+
+      context 'with unicode character at the end' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"'
+        end
+      end
+
+      context 'with escaped and not escaped unicode characters' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' }
+
+        it 'preserves escaped unicode characters' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;utf81=✓&amp;q=autolink"'
+        end
       end
     end
 
@@ -89,6 +115,22 @@ RSpec.describe Formatter do
       end
     end
 
+    context 'given a URL in quotation marks' do
+      let(:text) { '"https://example.com/"' }
+
+      it 'does not match the quotation marks' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
+    context 'given a URL in angle brackets' do
+      let(:text) { '<https://example.com/>' }
+
+      it 'does not match the angle brackets' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
     context 'given a URL with Japanese path string' do
       let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
 
@@ -105,6 +147,22 @@ RSpec.describe Formatter do
       end
     end
 
+    context 'given a URL with a full-width space' do
+      let(:text) { 'https://example.com/ abc123' }
+
+      it 'does not match the full-width space' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
+    context 'given a URL in Japanese quotation marks' do
+      let(:text) { '「[https://example.org/」' }
+
+      it 'does not match the quotation marks' do
+        is_expected.to include 'href="https://example.org/"'
+      end
+    end
+
     context 'given a URL with Simplified Chinese path string' do
       let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
 
@@ -124,7 +182,11 @@ RSpec.describe Formatter do
     context 'given a URL containing unsafe code (XSS attack, visible part)' do
       let(:text) { %q{http://example.com/b<del>b</del>} }
 
-      it 'escapes the HTML in the URL' do
+      it 'does not include the HTML in the URL' do
+        is_expected.to include '"http://example.com/b"'
+      end
+
+      it 'escapes the HTML' do
         is_expected.to include '&lt;del&gt;b&lt;/del&gt;'
       end
     end
@@ -132,7 +194,11 @@ RSpec.describe Formatter do
     context 'given a URL containing unsafe code (XSS attack, invisible part)' do
       let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
 
-      it 'escapes the HTML in the URL' do
+      it 'does not include the HTML in the URL' do
+        is_expected.to include '"http://example.com/blahblahblahblah/a"'
+      end
+
+      it 'escapes the HTML' do
         is_expected.to include '&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;'
       end
     end
@@ -168,6 +234,14 @@ RSpec.describe Formatter do
         is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
       end
     end
+
+    context 'given text containing a hashtag with Unicode chars' do
+      let(:text)  { '#hashtagタグ' }
+
+      it 'creates a hashtag link' do
+        is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagタグ</span></a>'
+      end
+    end
   end
 
   describe '#format_spoiler' do
diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb
index 9c9b87daf..36e346f14 100644
--- a/spec/models/concerns/account_interactions_spec.rb
+++ b/spec/models/concerns/account_interactions_spec.rb
@@ -244,9 +244,9 @@ describe AccountInteractions do
   end
 
   describe '#block_domain!' do
-    let(:domain_block) { Fabricate(:domain_block) }
+    let(:domain) { 'example.com' }
 
-    subject { account.block_domain!(domain_block) }
+    subject { account.block_domain!(domain) }
 
     it 'creates and returns AccountDomainBlock' do
       expect do
diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb
new file mode 100644
index 000000000..07533e0b9
--- /dev/null
+++ b/spec/models/featured_tag_spec.rb
@@ -0,0 +1,4 @@
+require 'rails_helper'
+
+RSpec.describe FeaturedTag, type: :model do
+end
diff --git a/spec/validators/email_mx_validator_spec.rb b/spec/validators/email_mx_validator_spec.rb
index bc68f63cf..48e17a4f1 100644
--- a/spec/validators/email_mx_validator_spec.rb
+++ b/spec/validators/email_mx_validator_spec.rb
@@ -11,6 +11,7 @@ describe EmailMxValidator do
 
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
       allow(resolver).to receive(:timeouts=).and_return(nil)
       allow(Resolv::DNS).to receive(:open).and_yield(resolver)
 
@@ -23,7 +24,9 @@ describe EmailMxValidator do
 
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
       allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([])
+      allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
       allow(resolver).to receive(:timeouts=).and_return(nil)
       allow(Resolv::DNS).to receive(:open).and_yield(resolver)
 
@@ -37,6 +40,21 @@ describe EmailMxValidator do
 
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '1.2.3.4')])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
+      allow(resolver).to receive(:timeouts=).and_return(nil)
+      allow(Resolv::DNS).to receive(:open).and_yield(resolver)
+
+      subject.validate(user)
+      expect(user.errors).to have_received(:add)
+    end
+
+    it 'adds an error if the AAAA record is blacklisted' do
+      EmailDomainBlock.create!(domain: 'fd00::1')
+      resolver = double
+
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::1')])
       allow(resolver).to receive(:timeouts=).and_return(nil)
       allow(Resolv::DNS).to receive(:open).and_yield(resolver)
 
@@ -50,7 +68,25 @@ describe EmailMxValidator do
 
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
       allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
+      allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
+      allow(resolver).to receive(:timeouts=).and_return(nil)
+      allow(Resolv::DNS).to receive(:open).and_yield(resolver)
+
+      subject.validate(user)
+      expect(user.errors).to have_received(:add)
+    end
+
+    it 'adds an error if the MX IPv6 record is blacklisted' do
+      EmailDomainBlock.create!(domain: 'fd00::2')
+      resolver = double
+
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
+      allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([])
+      allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')])
       allow(resolver).to receive(:timeouts=).and_return(nil)
       allow(Resolv::DNS).to receive(:open).and_yield(resolver)
 
@@ -64,7 +100,9 @@ describe EmailMxValidator do
 
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
       allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
+      allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
       allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
+      allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')])
       allow(resolver).to receive(:timeouts=).and_return(nil)
       allow(Resolv::DNS).to receive(:open).and_yield(resolver)