about summary refs log tree commit diff
path: root/spec/lib
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib')
-rw-r--r--spec/lib/activitypub/activity/accept_spec.rb38
-rw-r--r--spec/lib/activitypub/activity/announce_spec.rb29
-rw-r--r--spec/lib/activitypub/activity/block_spec.rb28
-rw-r--r--spec/lib/activitypub/activity/create_spec.rb221
-rw-r--r--spec/lib/activitypub/activity/delete_spec.rb53
-rw-r--r--spec/lib/activitypub/activity/follow_spec.rb49
-rw-r--r--spec/lib/activitypub/activity/like_spec.rb29
-rw-r--r--spec/lib/activitypub/activity/reject_spec.rb38
-rw-r--r--spec/lib/activitypub/activity/undo_spec.rb107
-rw-r--r--spec/lib/activitypub/activity/update_spec.rb41
-rw-r--r--spec/lib/activitypub/linked_data_signature_spec.rb82
-rw-r--r--spec/lib/activitypub/tag_manager_spec.rb99
-rw-r--r--spec/lib/formatter_spec.rb4
-rw-r--r--spec/lib/ostatus/atom_serializer_spec.rb42
-rw-r--r--spec/lib/status_finder_spec.rb (renamed from spec/lib/stream_entry_finder_spec.rb)24
-rw-r--r--spec/lib/tag_manager_spec.rb15
16 files changed, 856 insertions, 43 deletions
diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb
new file mode 100644
index 000000000..6503c83e3
--- /dev/null
+++ b/spec/lib/activitypub/activity/accept_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Accept do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Accept',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: {
+        id: 'bar',
+        type: 'Follow',
+        actor: ActivityPub::TagManager.instance.uri_for(recipient),
+        object: ActivityPub::TagManager.instance.uri_for(sender),
+      },
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      Fabricate(:follow_request, account: recipient, target_account: sender)
+      subject.perform
+    end
+
+    it 'creates a follow relationship' do
+      expect(recipient.following?(sender)).to be true
+    end
+
+    it 'removes the follow request' do
+      expect(recipient.requested?(sender)).to be false
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb
new file mode 100644
index 000000000..54dd52a60
--- /dev/null
+++ b/spec/lib/activitypub/activity/announce_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Announce do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+  let(:status)    { Fabricate(:status, account: recipient) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Announce',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(status),
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'creates a reblog by sender of status' do
+      expect(sender.reblogged?(status)).to be true
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/block_spec.rb b/spec/lib/activitypub/activity/block_spec.rb
new file mode 100644
index 000000000..23c8cc31c
--- /dev/null
+++ b/spec/lib/activitypub/activity/block_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Block do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Block',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(recipient),
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'creates a block from sender to recipient' do
+      expect(sender.blocking?(recipient)).to be true
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
new file mode 100644
index 000000000..fcb044ebc
--- /dev/null
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -0,0 +1,221 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Create do
+  let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Create',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: object_json,
+    }.with_indifferent_access
+  end
+
+  subject { described_class.new(json, sender) }
+
+  before do
+    stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
+  end
+
+  describe '#perform' do
+    before do
+      subject.perform
+    end
+
+    context 'standalone' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.text).to eq 'Lorem ipsum'
+      end
+
+      it 'missing to/cc defaults to direct privacy' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'direct'
+      end
+    end
+
+    context 'public' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          to: 'https://www.w3.org/ns/activitystreams#Public',
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'public'
+      end
+    end
+
+    context 'unlisted' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          cc: 'https://www.w3.org/ns/activitystreams#Public',
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'unlisted'
+      end
+    end
+
+    context 'private' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          to: 'http://example.com/followers',
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'private'
+      end
+    end
+
+    context 'direct' do
+      let(:recipient) { Fabricate(:account) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          to: ActivityPub::TagManager.instance.uri_for(recipient),
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.visibility).to eq 'direct'
+      end
+    end
+
+    context 'as a reply' do
+      let(:original_status) { Fabricate(:status) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.thread).to eq original_status
+        expect(status.reply?).to be true
+        expect(status.in_reply_to_account).to eq original_status.account
+        expect(status.conversation).to eq original_status.conversation
+      end
+    end
+
+    context 'with mentions' do
+      let(:recipient) { Fabricate(:account) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          tag: [
+            {
+              type: 'Mention',
+              href: ActivityPub::TagManager.instance.uri_for(recipient),
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.mentions.map(&:account)).to include(recipient)
+      end
+    end
+
+    context 'with media attachments' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          attachment: [
+            {
+              type: 'Document',
+              mime_type: 'image/png',
+              url: 'http://example.com/attachment.png',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
+      end
+    end
+
+    context 'with hashtags' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum',
+          tag: [
+            {
+              type: 'Hashtag',
+              href: 'http://example.com/blah',
+              name: '#test',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.tags.map(&:name)).to include('test')
+      end
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb
new file mode 100644
index 000000000..38254e31c
--- /dev/null
+++ b/spec/lib/activitypub/activity/delete_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Delete do
+  let(:sender)    { Fabricate(:account, domain: 'example.com') }
+  let(:status)    { Fabricate(:status, account: sender, uri: 'foobar') }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Delete',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(status),
+      signature: 'foo',
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'deletes sender\'s status' do
+      expect(Status.find_by(id: status.id)).to be_nil
+    end
+  end
+
+  context 'when the status has been reblogged' do
+    describe '#perform' do
+      subject { described_class.new(json, sender) }
+      let(:reblogger) { Fabricate(:account) }
+      let(:follower)   { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+
+      before do
+        stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+        follower.follow!(reblogger)
+        Fabricate(:status, account: reblogger, reblog: status)
+        subject.perform
+      end
+
+      it 'deletes sender\'s status' do
+        expect(Status.find_by(id: status.id)).to be_nil
+      end
+
+      it 'sends delete activity to followers of rebloggers' do
+        # one for Delete original post, and one for Undo reblog (normal delivery)
+        expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice
+      end
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb
new file mode 100644
index 000000000..6bbacdbe6
--- /dev/null
+++ b/spec/lib/activitypub/activity/follow_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Follow do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Follow',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(recipient),
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    context 'unlocked account' do
+      before do
+        subject.perform
+      end
+
+      it 'creates a follow from sender to recipient' do
+        expect(sender.following?(recipient)).to be true
+      end
+
+      it 'does not create a follow request' do
+        expect(sender.requested?(recipient)).to be false
+      end
+    end
+
+    context 'locked account' do
+      before do
+        recipient.update(locked: true)
+        subject.perform
+      end
+
+      it 'does not create a follow from sender to recipient' do
+        expect(sender.following?(recipient)).to be false
+      end
+
+      it 'creates a follow request' do
+        expect(sender.requested?(recipient)).to be true
+      end
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/like_spec.rb b/spec/lib/activitypub/activity/like_spec.rb
new file mode 100644
index 000000000..b69615a9d
--- /dev/null
+++ b/spec/lib/activitypub/activity/like_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Like do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+  let(:status)    { Fabricate(:status, account: recipient) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Like',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(status),
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'creates a favourite from sender to status' do
+      expect(sender.favourited?(status)).to be true
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/reject_spec.rb b/spec/lib/activitypub/activity/reject_spec.rb
new file mode 100644
index 000000000..7fd95bcc6
--- /dev/null
+++ b/spec/lib/activitypub/activity/reject_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Reject do
+  let(:sender)    { Fabricate(:account) }
+  let(:recipient) { Fabricate(:account) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Reject',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: {
+        id: 'bar',
+        type: 'Follow',
+        actor: ActivityPub::TagManager.instance.uri_for(recipient),
+        object: ActivityPub::TagManager.instance.uri_for(sender),
+      },
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      Fabricate(:follow_request, account: recipient, target_account: sender)
+      subject.perform
+    end
+
+    it 'does not create a follow relationship' do
+      expect(recipient.following?(sender)).to be false
+    end
+
+    it 'removes the follow request' do
+      expect(recipient.requested?(sender)).to be false
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/undo_spec.rb b/spec/lib/activitypub/activity/undo_spec.rb
new file mode 100644
index 000000000..14c68efe5
--- /dev/null
+++ b/spec/lib/activitypub/activity/undo_spec.rb
@@ -0,0 +1,107 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Undo do
+  let(:sender) { Fabricate(:account, domain: 'example.com') }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Undo',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: object_json,
+    }.with_indifferent_access
+  end
+
+  subject { described_class.new(json, sender) }
+
+  describe '#perform' do
+    context 'with Announce' do
+      let(:status) { Fabricate(:status) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Announce',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(status),
+        }
+      end
+
+      before do
+        Fabricate(:status, reblog: status, account: sender, uri: 'bar')
+      end
+
+      it 'deletes the reblog' do
+        subject.perform
+        expect(sender.reblogged?(status)).to be false
+      end
+    end
+
+    context 'with Block' do
+      let(:recipient) { Fabricate(:account) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Block',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(recipient),
+        }
+      end
+
+      before do
+        sender.block!(recipient)
+      end
+
+      it 'deletes block from sender to recipient' do
+        subject.perform
+        expect(sender.blocking?(recipient)).to be false
+      end
+    end
+
+    context 'with Follow' do
+      let(:recipient) { Fabricate(:account) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Follow',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(recipient),
+        }
+      end
+
+      before do
+        sender.follow!(recipient)
+      end
+
+      it 'deletes follow from sender to recipient' do
+        subject.perform
+        expect(sender.following?(recipient)).to be false
+      end
+    end
+
+    context 'with Like' do
+      let(:status) { Fabricate(:status) }
+
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Like',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: ActivityPub::TagManager.instance.uri_for(status),
+        }
+      end
+
+      before do
+        Fabricate(:favourite, account: sender, status: status)
+      end
+
+      it 'deletes favourite from sender to status' do
+        subject.perform
+        expect(sender.favourited?(status)).to be false
+      end
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb
new file mode 100644
index 000000000..0bd6d00d9
--- /dev/null
+++ b/spec/lib/activitypub/activity/update_spec.rb
@@ -0,0 +1,41 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Update do
+  let!(:sender) { Fabricate(:account) }
+  
+  before do
+    sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender))
+  end
+
+  let(:modified_sender) do 
+    sender.dup.tap do |modified_sender|
+      modified_sender.display_name = 'Totally modified now'
+    end
+  end
+
+  let(:actor_json) do
+    ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json
+  end
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Update',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: actor_json,
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'updates profile' do
+      expect(sender.reload.display_name).to eq 'Totally modified now'
+    end
+  end
+end
diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb
new file mode 100644
index 000000000..a4d6fe8c3
--- /dev/null
+++ b/spec/lib/activitypub/linked_data_signature_spec.rb
@@ -0,0 +1,82 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::LinkedDataSignature do
+  include JsonLdHelper
+
+  let!(:sender) { Fabricate(:account, uri: 'http://example.com/alice') }
+
+  let(:raw_json) do
+    {
+      '@context' => 'https://www.w3.org/ns/activitystreams',
+      'id' => 'http://example.com/hello-world',
+    }
+  end
+
+  let(:json) { raw_json.merge('signature' => signature) }
+
+  subject { described_class.new(json) }
+
+  describe '#verify_account!' do
+    context 'when signature matches' do
+      let(:raw_signature) do
+        {
+          'creator' => 'http://example.com/alice',
+          'created' => '2017-09-23T20:21:34Z',
+        }
+      end
+
+      let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
+
+      it 'returns creator' do
+        expect(subject.verify_account!).to eq sender
+      end
+    end
+
+    context 'when signature is missing' do
+      let(:signature) { nil }
+
+      it 'returns nil' do
+        expect(subject.verify_account!).to be_nil
+      end
+    end
+
+    context 'when signature is tampered' do
+      let(:raw_signature) do
+        {
+          'creator' => 'http://example.com/alice',
+          'created' => '2017-09-23T20:21:34Z',
+        }
+      end
+
+      let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') }
+
+      it 'returns nil' do
+        expect(subject.verify_account!).to be_nil
+      end
+    end
+  end
+
+  describe '#sign!' do
+    subject { described_class.new(raw_json).sign!(sender) }
+
+    it 'returns a hash' do
+      expect(subject).to be_a Hash
+    end
+
+    it 'contains signature' do
+      expect(subject['signature']).to be_a Hash
+      expect(subject['signature']['signatureValue']).to be_present
+    end
+
+    it 'can be verified again' do
+      expect(described_class.new(subject).verify_account!).to eq sender
+    end
+  end
+
+  def sign(from_account, options, document)
+    options_hash   = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT)))
+    document_hash  = Digest::SHA256.hexdigest(canonicalize(document))
+    to_be_verified = options_hash + document_hash
+    Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_verified))
+  end
+end
diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb
new file mode 100644
index 000000000..8f7662e24
--- /dev/null
+++ b/spec/lib/activitypub/tag_manager_spec.rb
@@ -0,0 +1,99 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::TagManager do
+  include RoutingHelper
+
+  subject { described_class.instance }
+
+  describe '#url_for' do
+    it 'returns a string' do
+      account = Fabricate(:account)
+      expect(subject.url_for(account)).to be_a String
+    end
+  end
+
+  describe '#uri_for' do
+    it 'returns a string' do
+      account = Fabricate(:account)
+      expect(subject.uri_for(account)).to be_a String
+    end
+  end
+
+  describe '#to' do
+    it 'returns public collection for public status' do
+      status = Fabricate(:status, visibility: :public)
+      expect(subject.to(status)).to eq ['https://www.w3.org/ns/activitystreams#Public']
+    end
+
+    it 'returns followers collection for unlisted status' do
+      status = Fabricate(:status, visibility: :unlisted)
+      expect(subject.to(status)).to eq [account_followers_url(status.account)]
+    end
+
+    it 'returns followers collection for private status' do
+      status = Fabricate(:status, visibility: :private)
+      expect(subject.to(status)).to eq [account_followers_url(status.account)]
+    end
+
+    it 'returns URIs of mentions for direct status' do
+      status    = Fabricate(:status, visibility: :direct)
+      mentioned = Fabricate(:account)
+      status.mentions.create(account: mentioned)
+      expect(subject.to(status)).to eq [subject.uri_for(mentioned)]
+    end
+  end
+
+  describe '#cc' do
+    it 'returns followers collection for public status' do
+      status = Fabricate(:status, visibility: :public)
+      expect(subject.cc(status)).to eq [account_followers_url(status.account)]
+    end
+
+    it 'returns public collection for unlisted status' do
+      status = Fabricate(:status, visibility: :unlisted)
+      expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public']
+    end
+
+    it 'returns empty array for private status' do
+      status = Fabricate(:status, visibility: :private)
+      expect(subject.cc(status)).to eq []
+    end
+
+    it 'returns empty array for direct status' do
+      status = Fabricate(:status, visibility: :direct)
+      expect(subject.cc(status)).to eq []
+    end
+
+    it 'returns URIs of mentions for non-direct status' do
+      status    = Fabricate(:status, visibility: :public)
+      mentioned = Fabricate(:account)
+      status.mentions.create(account: mentioned)
+      expect(subject.cc(status)).to include(subject.uri_for(mentioned))
+    end
+  end
+
+  describe '#local_uri?' do
+    it 'returns false for non-local URI' do
+      expect(subject.local_uri?('http://example.com/123')).to be false
+    end
+
+    it 'returns true for local URIs' do
+      account = Fabricate(:account)
+      expect(subject.local_uri?(subject.uri_for(account))).to be true
+    end
+  end
+
+  describe '#uri_to_local_id' do
+    it 'returns the local ID' do
+      account = Fabricate(:account)
+      expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username
+    end
+  end
+
+  describe '#uri_to_resource' do
+    it 'returns the local resource' do
+      account = Fabricate(:account)
+      expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account
+    end
+  end
+end
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
index dfe1d8b8f..ab04ccbab 100644
--- a/spec/lib/formatter_spec.rb
+++ b/spec/lib/formatter_spec.rb
@@ -178,7 +178,7 @@ RSpec.describe Formatter do
     end
 
     context 'with remote status' do
-      let(:status) { Fabricate(:status, text: 'Beep boop', uri: 'beepboop') }
+      let(:status) { Fabricate(:status, account: remote_account, text: 'Beep boop') }
 
       it 'reformats' do
         is_expected.to eq 'Beep boop'
@@ -226,7 +226,7 @@ RSpec.describe Formatter do
     end
 
     context 'with remote status' do
-      let(:status)  { Fabricate(:status, text: '<script>alert("Hello")</script>', uri: 'beep boop') }
+      let(:status)  { Fabricate(:status, account: remote_account, text: '<script>alert("Hello")</script>') }
 
       it 'returns tag-stripped text' do
         is_expected.to eq ''
diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb
index b0cb8f019..0451eceeb 100644
--- a/spec/lib/ostatus/atom_serializer_spec.rb
+++ b/spec/lib/ostatus/atom_serializer_spec.rb
@@ -196,7 +196,7 @@ RSpec.describe OStatus::AtomSerializer do
 
       author = OStatus::AtomSerializer.new.author(account)
 
-      link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+      link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
       expect(link[:rel]).to eq 'alternate'
       expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username'
@@ -403,10 +403,10 @@ RSpec.describe OStatus::AtomSerializer do
 
       it 'returns element whose rendered view triggers creation when processed' do
         remote_account = Account.create!(username: 'username')
-        remote_status = Fabricate(:status, account: remote_account)
-        remote_status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
+        remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z')
 
         entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true)
+        entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test
         xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote')
 
         remote_status.destroy!
@@ -415,12 +415,12 @@ RSpec.describe OStatus::AtomSerializer do
         account = Account.create!(
           domain: 'remote',
           username: 'username',
-          last_webfingered_at: Time.now.utc,
+          last_webfingered_at: Time.now.utc
         )
 
         ProcessFeedService.new.call(xml, account)
 
-        expect(Status.find_by(uri: "tag:remote,2000-01-01:objectId=#{remote_status.id}:objectType=Status")).to be_instance_of Status
+        expect(Status.find_by(uri: "https://remote/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status
       end
     end
 
@@ -464,12 +464,11 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends id element with unique tag' do
-      status = Fabricate(:status, reblog_of_id: nil)
-      status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
+      status = Fabricate(:status, reblog_of_id: nil, created_at: '2000-01-01T00:00:00Z')
 
       entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
 
-      expect(entry.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(entry.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends published element with created date' do
@@ -514,7 +513,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.entry(reblog.stream_entry)
 
       object = entry.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{reblogged.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{reblogged.account.to_param}/statuses/#{reblogged.id}"
     end
 
     it 'does not append activity:object element if target is not present' do
@@ -529,9 +528,9 @@ RSpec.describe OStatus::AtomSerializer do
 
       entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
 
-      link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+      link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
-      expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}"
+      expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
     it 'appends link element for itself' do
@@ -552,7 +551,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.entry(reply_status.stream_entry)
 
       in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{in_reply_to_status.id}:objectType=Status"
+      expect(in_reply_to[:ref]).to eq "https://cb6e6126.ngrok.io/users/#{in_reply_to_status.account.to_param}/statuses/#{in_reply_to_status.id}"
     end
 
     it 'does not append thr:in-reply-to element if not threaded' do
@@ -642,7 +641,7 @@ RSpec.describe OStatus::AtomSerializer do
 
       feed = OStatus::AtomSerializer.new.feed(account, [])
 
-      link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+      link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
       expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username'
     end
@@ -933,7 +932,7 @@ RSpec.describe OStatus::AtomSerializer do
       favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
 
       object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends thr:in-reply-to element for status' do
@@ -944,7 +943,7 @@ RSpec.describe OStatus::AtomSerializer do
       favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
 
       in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
@@ -1033,7 +1032,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
 
       object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends thr:in-reply-to element for status' do
@@ -1044,7 +1043,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
 
       in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
@@ -1452,7 +1451,7 @@ RSpec.describe OStatus::AtomSerializer do
     it 'appends id element with URL for status' do
       status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
       object = OStatus::AtomSerializer.new.object(status)
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends published element with created date' do
@@ -1462,7 +1461,8 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends updated element with updated date' do
-      status = Fabricate(:status, updated_at: '2000-01-01T00:00:00Z')
+      status = Fabricate(:status)
+      status.updated_at = '2000-01-01T00:00:00Z'
       object = OStatus::AtomSerializer.new.object(status)
       expect(object.updated.text).to eq '2000-01-01T00:00:00Z'
     end
@@ -1509,7 +1509,7 @@ RSpec.describe OStatus::AtomSerializer do
 
       entry = OStatus::AtomSerializer.new.object(status)
 
-      link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+      link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
       expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
@@ -1522,7 +1522,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.object(reply)
 
       in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{thread.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{thread.account.to_param}/statuses/#{thread.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}"
     end
 
diff --git a/spec/lib/stream_entry_finder_spec.rb b/spec/lib/status_finder_spec.rb
index 64e03c36a..3ef086736 100644
--- a/spec/lib/stream_entry_finder_spec.rb
+++ b/spec/lib/status_finder_spec.rb
@@ -2,17 +2,17 @@
 
 require 'rails_helper'
 
-describe StreamEntryFinder do
+describe StatusFinder do
   include RoutingHelper
 
-  describe '#stream_entry' do
+  describe '#status' do
     context 'with a status url' do
       let(:status) { Fabricate(:status) }
       let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) }
       subject { described_class.new(url) }
 
       it 'finds the stream entry' do
-        expect(subject.stream_entry).to eq(status.stream_entry)
+        expect(subject.status).to eq(status)
       end
 
       it 'raises an error if action is not :show' do
@@ -20,7 +20,7 @@ describe StreamEntryFinder do
         expect(recognized).to receive(:[]).with(:action).and_return(:create)
         expect(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized)
 
-        expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+        expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
 
@@ -30,7 +30,17 @@ describe StreamEntryFinder do
       subject { described_class.new(url) }
 
       it 'finds the stream entry' do
-        expect(subject.stream_entry).to eq(stream_entry)
+        expect(subject.status).to eq(stream_entry.status)
+      end
+    end
+
+    context 'with a remote url even if id exists on local' do
+      let(:status) { Fabricate(:status) }
+      let(:url) { "https://example.com/users/test/statuses/#{status.id}" }
+      subject { described_class.new(url) }
+
+      it 'raises an error' do
+        expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
 
@@ -39,7 +49,7 @@ describe StreamEntryFinder do
       subject { described_class.new(url) }
 
       it 'raises an error' do
-        expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+        expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
 
@@ -48,7 +58,7 @@ describe StreamEntryFinder do
       subject { described_class.new(url) }
 
       it 'raises an error' do
-        expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+        expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
   end
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index 1fae6bec4..1cd6e0a6f 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -157,23 +157,12 @@ RSpec.describe TagManager do
   describe '#uri_for' do
     subject { TagManager.instance.uri_for(target) }
 
-    context 'activity object' do
-      let(:target) { Fabricate(:status, reblog: Fabricate(:status)).stream_entry }
-
-      before { target.update!(created_at: '2000-01-01T00:00:00Z') }
-
-      it 'returns the unique tag for status' do
-        expect(target.object_type).to eq :activity
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
-      end
-    end
-
     context 'comment object' do
       let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) }
 
       it 'returns the unique tag for status' do
         expect(target.object_type).to eq :comment
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
+        is_expected.to eq target.uri
       end
     end
 
@@ -182,7 +171,7 @@ RSpec.describe TagManager do
 
       it 'returns the unique tag for status' do
         expect(target.object_type).to eq :note
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
+        is_expected.to eq target.uri
       end
     end