about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2020-05-03 21:20:42 +0200
committerThibaut Girka <thib@sitedethib.com>2020-05-03 21:23:49 +0200
commita22e6a368333f3563f8d8d56d8e98d02088e82dc (patch)
tree4146f9e8afe4257c6f33bc695a3cfbfb89aa81b6 /spec
parent9c61dadc0db7009853c6b2345a02c3b219022929 (diff)
parente223fd8c6190661237ea43e7773e47513c48fd46 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- `app/controllers/statuses_controller.rb`:
  Upstream disabled the embed controller for reblogs.
  Not a real conflict, but glitch-soc has an extra line to deal
  with its theming system.
  Ported upstream changes.
- `app/javascript/packs/public.js`:
  Upstream made changes to get rid of most inline CSS, this changes
  javascript for public pages, which in glitch are split between
  different files. Ported those changes.
- `app/models/status.rb`:
  Upstream changed the block check in `Status#permitted_for` to
  include domain-block checks. Not a real conflict with glitch-soc,
  but our scope is slightly different, as our scope for
  unauthenticated access do not include instance-local toots.
  Ported upstream changes.
- `app/serializers/rest/instance_serializer.rb`:
  Not a real conflict, upstream added a new field to the instance
  serializer, the conflict is one line above since we added more of
  that.
  Ported upstream changes.
- `app/views/settings/profiles/show.html.haml`:
  Upstream got rid of most inline CSS and moved hidden elements
  to data attributes in the process, in fields were we have
  different values.
  Ported upstream changes while keeping our glitch-specific
  values.
- `app/views/statuses/_simple_status.html.haml`:
  Upstream got rid of inline CSS on an HAML line we treat
  differently, stripping empty text nodes.
  Ported upstream changes to the style attribute, keeping
  the empty text node stripping behavior.
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/activitypub/collections_controller_spec.rb132
-rw-r--r--spec/controllers/activitypub/inboxes_controller_spec.rb28
-rw-r--r--spec/controllers/activitypub/outboxes_controller_spec.rb170
-rw-r--r--spec/controllers/activitypub/replies_controller_spec.rb196
-rw-r--r--spec/controllers/statuses_controller_spec.rb843
-rw-r--r--spec/fabricators/status_pin_fabricator.rb2
-rw-r--r--spec/models/status_spec.rb12
-rw-r--r--spec/services/fetch_resource_service_spec.rb12
8 files changed, 1280 insertions, 115 deletions
diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb
index 34114cc85..56be49be3 100644
--- a/spec/controllers/activitypub/collections_controller_spec.rb
+++ b/spec/controllers/activitypub/collections_controller_spec.rb
@@ -3,21 +3,133 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::CollectionsController, type: :controller do
-  describe 'POST #show' do
-    let(:account) { Fabricate(:account) }
+  let!(:account) { Fabricate(:account) }
+  let(:remote_account) { nil }
 
-    context 'id is "featured"' do
-      it 'returns 200 with "application/activity+json"' do
-        post :show, params: { id: 'featured', account_username: account.username }
+  before do
+    allow(controller).to receive(:signed_request_account).and_return(remote_account)
 
-        expect(response).to have_http_status(200)
-        expect(response.content_type).to eq 'application/activity+json'
+    Fabricate(:status_pin, account: account)
+    Fabricate(:status_pin, account: account)
+    Fabricate(:status, account: account, visibility: :private)
+  end
+
+  describe 'GET #show' do
+    context 'when id is "featured"' do
+      context 'without signature' do
+        let(:remote_account) { nil }
+
+        before do
+          get :show, params: { id: 'featured', account_username: account.username }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+
+        it 'returns orderedItems with pinned statuses' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 2
+        end
+      end
+
+      context 'with signature' do
+        let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+
+        context do
+          before do
+            get :show, params: { id: 'featured', account_username: account.username }
+          end
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns application/activity+json' do
+            expect(response.content_type).to eq 'application/activity+json'
+          end
+
+          it 'returns public Cache-Control header' do
+            expect(response.headers['Cache-Control']).to include 'public'
+          end
+
+          it 'returns orderedItems with pinned statuses' do
+            json = body_as_json
+            expect(json[:orderedItems]).to be_an Array
+            expect(json[:orderedItems].size).to eq 2
+          end
+        end
+
+        context 'in authorized fetch mode' do
+          before do
+            allow(controller).to receive(:authorized_fetch_mode?).and_return(true)
+          end
+
+          context 'when signed request account is blocked' do
+            before do
+              account.block!(remote_account)
+              get :show, params: { id: 'featured', account_username: account.username }
+            end
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns application/activity+json' do
+              expect(response.content_type).to eq 'application/activity+json'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns empty orderedItems' do
+              json = body_as_json
+              expect(json[:orderedItems]).to be_an Array
+              expect(json[:orderedItems].size).to eq 0
+            end
+          end
+
+          context 'when signed request account is domain blocked' do
+            before do
+              account.block_domain!(remote_account.domain)
+              get :show, params: { id: 'featured', account_username: account.username }
+            end
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns application/activity+json' do
+              expect(response.content_type).to eq 'application/activity+json'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns empty orderedItems' do
+              json = body_as_json
+              expect(json[:orderedItems]).to be_an Array
+              expect(json[:orderedItems].size).to eq 0
+            end
+          end
+        end
       end
     end
 
-    context 'id is not "featured"' do
-      it 'returns 404' do
-        post :show, params: { id: 'hoge', account_username: account.username }
+    context 'when id is not "featured"' do
+      it 'returns http not found' do
+        get :show, params: { id: 'hoge', account_username: account.username }
         expect(response).to have_http_status(404)
       end
     end
diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb
index a9ee75490..f3bc23953 100644
--- a/spec/controllers/activitypub/inboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/inboxes_controller_spec.rb
@@ -3,25 +3,31 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::InboxesController, type: :controller do
+  let(:remote_account) { nil }
+
+  before do
+    allow(controller).to receive(:signed_request_account).and_return(remote_account)
+  end
+
   describe 'POST #create' do
-    context 'with signed_request_account' do
-      it 'returns 202' do
-        allow(controller).to receive(:signed_request_account) do
-          Fabricate(:account)
-        end
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) }
 
+      before do
         post :create, body: '{}'
+      end
+
+      it 'returns http accepted' do
         expect(response).to have_http_status(202)
       end
     end
 
-    context 'without signed_request_account' do
-      it 'returns 401' do
-        allow(controller).to receive(:signed_request_account) do
-          false
-        end
-
+    context 'without signature' do
+      before do
         post :create, body: '{}'
+      end
+
+      it 'returns http not authorized' do
         expect(response).to have_http_status(401)
       end
     end
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
index 47460b22c..03490533d 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -4,20 +4,174 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
   let!(:account) { Fabricate(:account) }
 
   before do
-    Fabricate(:status, account: account)
+    Fabricate(:status, account: account, visibility: :public)
+    Fabricate(:status, account: account, visibility: :unlisted)
+    Fabricate(:status, account: account, visibility: :private)
+    Fabricate(:status, account: account, visibility: :direct)
+    Fabricate(:status, account: account, visibility: :limited)
+  end
+
+  before do
+    allow(controller).to receive(:signed_request_account).and_return(remote_account)
   end
 
   describe 'GET #show' do
-    before do
-      get :show, params: { account_username: account.username }
-    end
+    context 'without signature' do
+      let(:remote_account) { nil }
+
+      before do
+        get :show, params: { account_username: account.username, page: page }
+      end
+
+      context 'with page not requested' do
+        let(:page) { nil }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns totalItems' do
+          json = body_as_json
+          expect(json[:totalItems]).to eq 4
+        end
 
-    it 'returns http success' do
-      expect(response).to have_http_status(200)
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+      end
+
+      context 'with page requested' do
+        let(:page) { 'true' }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns orderedItems with public or unlisted statuses' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 2
+          expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+      end
     end
 
-    it 'returns application/activity+json' do
-      expect(response.content_type).to eq 'application/activity+json'
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+      let(:page) { 'true' }
+
+      context 'when signed request account does not follow account' do
+        before do
+          get :show, params: { account_username: account.username, page: page }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns orderedItems with public or unlisted statuses' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 2
+          expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+        end
+
+        it 'returns private Cache-Control header' do
+          expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+        end
+      end
+
+      context 'when signed request account follows account' do
+        before do
+          remote_account.follow!(account)
+          get :show, params: { account_username: account.username, page: page }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns orderedItems with private statuses' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 3
+          expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:to].include?(account_followers_url(account, ActionMailer::Base.default_url_options)) }).to be true
+        end
+
+        it 'returns private Cache-Control header' do
+          expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+        end
+      end
+
+      context 'when signed request account is blocked' do
+        before do
+          account.block!(remote_account)
+          get :show, params: { account_username: account.username, page: page }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns empty orderedItems' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 0
+        end
+
+        it 'returns private Cache-Control header' do
+          expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+        end
+      end
+
+      context 'when signed request account is domain blocked' do
+        before do
+          account.block_domain!(remote_account.domain)
+          get :show, params: { account_username: account.username, page: page }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns empty orderedItems' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 0
+        end
+
+        it 'returns private Cache-Control header' do
+          expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+        end
+      end
     end
   end
 end
diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb
new file mode 100644
index 000000000..a5ed14180
--- /dev/null
+++ b/spec/controllers/activitypub/replies_controller_spec.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ActivityPub::RepliesController, type: :controller do
+  let(:status) { Fabricate(:status, visibility: parent_visibility) }
+  let(:remote_account) { nil }
+
+  before do
+    allow(controller).to receive(:signed_request_account).and_return(remote_account)
+
+    Fabricate(:status, thread: status, visibility: :public)
+    Fabricate(:status, thread: status, visibility: :public)
+    Fabricate(:status, thread: status, visibility: :private)
+    Fabricate(:status, account: status.account, thread: status, visibility: :public)
+    Fabricate(:status, account: status.account, thread: status, visibility: :private)
+  end
+
+  describe 'GET #index' do
+    context 'with no signature' do
+      before do
+        get :index, params: { account_username: status.account.username, status_id: status.id }
+      end
+
+      context 'when status is public' do
+        let(:parent_visibility) { :public }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+
+        it 'returns items with account\'s own replies' do
+          json = body_as_json
+
+          expect(json[:first]).to be_a Hash
+          expect(json[:first][:items]).to be_an Array
+          expect(json[:first][:items].size).to eq 1
+          expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+        end
+      end
+
+      context 'when status is private' do
+        let(:parent_visibility) { :private }
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when status is direct' do
+        let(:parent_visibility) { :direct }
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+    end
+
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+      let(:only_other_accounts) { nil }
+
+      context do
+        before do
+          get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts }
+        end
+
+        context 'when status is public' do
+          let(:parent_visibility) { :public }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns application/activity+json' do
+            expect(response.content_type).to eq 'application/activity+json'
+          end
+
+          it 'returns public Cache-Control header' do
+            expect(response.headers['Cache-Control']).to include 'public'
+          end
+
+          context 'without only_other_accounts' do
+            it 'returns items with account\'s own replies' do
+              json = body_as_json
+
+              expect(json[:first]).to be_a Hash
+              expect(json[:first][:items]).to be_an Array
+              expect(json[:first][:items].size).to eq 1
+              expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+            end
+          end
+
+          context 'with only_other_accounts' do
+            let(:only_other_accounts) { 'true' }
+
+            it 'returns items with other public or unlisted replies' do
+              json = body_as_json
+
+              expect(json[:first]).to be_a Hash
+              expect(json[:first][:items]).to be_an Array
+              expect(json[:first][:items].size).to eq 2
+              expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+            end
+          end
+        end
+
+        context 'when status is private' do
+          let(:parent_visibility) { :private }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is direct' do
+          let(:parent_visibility) { :direct }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
+
+      context 'when signed request account is blocked' do
+        before do
+          status.account.block!(remote_account)
+          get :index, params: { account_username: status.account.username, status_id: status.id }
+        end
+
+        context 'when status is public' do
+          let(:parent_visibility) { :public }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is private' do
+          let(:parent_visibility) { :private }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is direct' do
+          let(:parent_visibility) { :direct }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
+
+      context 'when signed request account is domain blocked' do
+        before do
+          status.account.block_domain!(remote_account.domain)
+          get :index, params: { account_username: status.account.username, status_id: status.id }
+        end
+
+        context 'when status is public' do
+          let(:parent_visibility) { :public }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is private' do
+          let(:parent_visibility) { :private }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is direct' do
+          let(:parent_visibility) { :direct }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 6905dae10..ba1f1370a 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -5,128 +5,821 @@ require 'rails_helper'
 describe StatusesController do
   render_views
 
-  describe '#show' do
-    context 'account is suspended' do
-      it 'returns gone' do
-        account = Fabricate(:account, suspended: true)
-        status = Fabricate(:status, account: account)
+  describe 'GET #show' do
+    let(:account) { Fabricate(:account) }
+    let(:status)  { Fabricate(:status, account: account) }
 
+    context 'when account is suspended' do
+      let(:account) { Fabricate(:account, suspended: true) }
+
+      before do
         get :show, params: { account_username: account.username, id: status.id }
+      end
 
+      it 'returns http gone' do
         expect(response).to have_http_status(410)
       end
     end
 
-    context 'status is not permitted' do
-      it 'raises ActiveRecord::RecordNotFound' do
-        user = Fabricate(:user)
-        status = Fabricate(:status)
-        status.account.block!(user.account)
+    context 'when status is a reblog' do
+      let(:original_account) { Fabricate(:account, domain: 'example.com') }
+      let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
+      let(:status) { Fabricate(:status, account: account, reblog: original_status) }
 
-        sign_in(user)
+      before do
         get :show, params: { account_username: status.account.username, id: status.id }
+      end
 
-        expect(response).to have_http_status(404)
+      it 'redirects to the original status' do
+        expect(response).to redirect_to(original_status.url)
       end
     end
 
-    context 'status is a reblog' do
-      it 'redirects to the original status' do
-        original_account = Fabricate(:account, domain: 'example.com')
-        original_status = Fabricate(:status, account: original_account, uri: 'tag:example.com,2017:foo', url: 'https://example.com/123')
-        status = Fabricate(:status, reblog: original_status)
+    context 'when status is public' do
+      before do
+        get :show, params: { account_username: status.account.username, id: status.id, format: format }
+      end
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+      context 'as HTML' do
+        let(:format) { 'html' }
 
-        expect(response).to redirect_to(original_status.url)
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns Link header' do
+          expect(response.headers['Link'].to_s).to include 'activity+json'
+        end
+
+        it 'returns Vary header' do
+          expect(response.headers['Vary']).to eq 'Accept'
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+
+        it 'renders status' do
+          expect(response).to render_template(:show)
+          expect(response.body).to include status.text
+        end
+      end
+
+      context 'as JSON' do
+        let(:format) { 'json' }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns Link header' do
+          expect(response.headers['Link'].to_s).to include 'activity+json'
+        end
+
+        it 'returns Vary header' do
+          expect(response.headers['Vary']).to eq 'Accept'
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+
+        it 'returns Content-Type header' do
+          expect(response.headers['Content-Type']).to include 'application/activity+json'
+        end
+
+        it 'renders ActivityPub Note object' do
+          json = body_as_json
+          expect(json[:content]).to include status.text
+        end
       end
     end
 
-    context 'account is not suspended and status is permitted' do
-      it 'assigns @account' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
-        expect(assigns(:account)).to eq status.account
+    context 'when status is private' do
+      let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+      before do
+        get :show, params: { account_username: status.account.username, id: status.id, format: format }
       end
 
-      it 'assigns @status' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
-        expect(assigns(:status)).to eq status
+      context 'as JSON' do
+        let(:format) { 'json' }
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it 'assigns @ancestors for ancestors of the status if it is a reply' do
-        ancestor = Fabricate(:status)
-        status = Fabricate(:status, in_reply_to_id: ancestor.id)
+      context 'as HTML' do
+        let(:format) { 'html' }
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+    end
+
+    context 'when status is direct' do
+      let(:status) { Fabricate(:status, account: account, visibility: :direct) }
 
-        expect(assigns(:ancestors)).to eq [ancestor]
+      before do
+        get :show, params: { account_username: status.account.username, id: status.id, format: format }
       end
 
-      it 'assigns @ancestors for [] if it is not a reply' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
-        expect(assigns(:ancestors)).to eq []
+      context 'as JSON' do
+        let(:format) { 'json' }
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it 'assigns @descendant_threads for a thread with several statuses' do
-        status = Fabricate(:status)
-        child = Fabricate(:status, in_reply_to_id: status.id)
-        grandchild = Fabricate(:status, in_reply_to_id: child.id)
+      context 'as HTML' do
+        let(:format) { 'html' }
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+    end
+
+    context 'when signed-in' do
+      let(:user) { Fabricate(:user) }
 
-        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchild.id]
+      before do
+        sign_in(user)
       end
 
-      it 'assigns @descendant_threads for several threads sharing the same descendant' do
-        status = Fabricate(:status)
-        child = Fabricate(:status, in_reply_to_id: status.id)
-        grandchildren = 2.times.map { Fabricate(:status, in_reply_to_id: child.id) }
+      context 'when account blocks user' do
+        before do
+          account.block!(user.account)
+          get :show, params: { account_username: status.account.username, id: status.id }
+        end
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when status is public' do
+        before do
+          get :show, params: { account_username: status.account.username, id: status.id, format: format }
+        end
+
+        context 'as HTML' do
+          let(:format) { 'html' }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns Link header' do
+            expect(response.headers['Link'].to_s).to include 'activity+json'
+          end
 
-        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchildren[0].id]
-        expect(assigns(:descendant_threads)[1][:statuses].pluck(:id)).to eq [grandchildren[1].id]
+          it 'returns Vary header' do
+            expect(response.headers['Vary']).to eq 'Accept'
+          end
+
+          it 'returns no Cache-Control header' do
+            expect(response.headers).to_not include 'Cache-Control'
+          end
+
+          it 'renders status' do
+            expect(response).to render_template(:show)
+            expect(response.body).to include status.text
+          end
+        end
+
+        context 'as JSON' do
+          let(:format) { 'json' }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns Link header' do
+            expect(response.headers['Link'].to_s).to include 'activity+json'
+          end
+
+          it 'returns Vary header' do
+            expect(response.headers['Vary']).to eq 'Accept'
+          end
+
+          it 'returns public Cache-Control header' do
+            expect(response.headers['Cache-Control']).to include 'public'
+          end
+
+          it 'returns Content-Type header' do
+            expect(response.headers['Content-Type']).to include 'application/activity+json'
+          end
+
+          it 'renders ActivityPub Note object' do
+            json = body_as_json
+            expect(json[:content]).to include status.text
+          end
+        end
       end
 
-      it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do
-        stub_const 'StatusControllerConcern::DESCENDANTS_LIMIT', 1
-        status = Fabricate(:status)
-        child = Fabricate(:status, in_reply_to_id: status.id)
+      context 'when status is private' do
+        let(:status) { Fabricate(:status, account: account, visibility: :private) }
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        context 'when user is authorized to see it' do
+          before do
+            user.account.follow!(account)
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
 
-        expect(assigns(:descendant_threads)).to eq []
-        expect(assigns(:max_descendant_thread_id)).to eq child.id
+            it 'returns no Cache-Control header' do
+              expect(response.headers).to_not include 'Cache-Control'
+            end
+
+            it 'renders status' do
+              expect(response).to render_template(:show)
+              expect(response.body).to include status.text
+            end
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns Content-Type header' do
+              expect(response.headers['Content-Type']).to include 'application/activity+json'
+            end
+
+            it 'renders ActivityPub Note object' do
+              json = body_as_json
+              expect(json[:content]).to include status.text
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          before do
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
       end
 
-      it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do
-        stub_const 'StatusControllerConcern::DESCENDANTS_DEPTH_LIMIT', 2
-        status = Fabricate(:status)
-        child0 = Fabricate(:status, in_reply_to_id: status.id)
-        child1 = Fabricate(:status, in_reply_to_id: child0.id)
-        child2 = Fabricate(:status, in_reply_to_id: child0.id)
+      context 'when status is direct' do
+        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        context 'when user is authorized to see it' do
+          before do
+            Fabricate(:mention, account: user.account, status: status)
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns no Cache-Control header' do
+              expect(response.headers).to_not include 'Cache-Control'
+            end
+
+            it 'renders status' do
+              expect(response).to render_template(:show)
+              expect(response.body).to include status.text
+            end
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns Content-Type header' do
+              expect(response.headers['Content-Type']).to include 'application/activity+json'
+            end
+
+            it 'renders ActivityPub Note object' do
+              json = body_as_json
+              expect(json[:content]).to include status.text
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          before do
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
 
-        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).not_to include child1.id
-        expect(assigns(:descendant_threads)[1][:statuses].pluck(:id)).not_to include child2.id
-        expect(assigns(:descendant_threads)[0][:next_status].id).to eq child1.id
-        expect(assigns(:descendant_threads)[1][:next_status].id).to eq child2.id
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
       end
+    end
 
-      it 'returns a success' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+
+      before do
+        allow(controller).to receive(:signed_request_account).and_return(remote_account)
+      end
+
+      context 'when account blocks account' do
+        before do
+          account.block!(remote_account)
+          get :show, params: { account_username: status.account.username, id: status.id }
+        end
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when account domain blocks account' do
+        before do
+          account.block_domain!(remote_account.domain)
+          get :show, params: { account_username: status.account.username, id: status.id }
+        end
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when status is public' do
+        before do
+          get :show, params: { account_username: status.account.username, id: status.id, format: format }
+        end
+
+        context 'as HTML' do
+          let(:format) { 'html' }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns Link header' do
+            expect(response.headers['Link'].to_s).to include 'activity+json'
+          end
+
+          it 'returns Vary header' do
+            expect(response.headers['Vary']).to eq 'Accept'
+          end
+
+          it 'returns no Cache-Control header' do
+            expect(response.headers).to_not include 'Cache-Control'
+          end
+
+          it 'renders status' do
+            expect(response).to render_template(:show)
+            expect(response.body).to include status.text
+          end
+        end
+
+        context 'as JSON' do
+          let(:format) { 'json' }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns Link header' do
+            expect(response.headers['Link'].to_s).to include 'activity+json'
+          end
+
+          it 'returns Vary header' do
+            expect(response.headers['Vary']).to eq 'Accept'
+          end
+
+          it 'returns public Cache-Control header' do
+            expect(response.headers['Cache-Control']).to include 'public'
+          end
+
+          it 'returns Content-Type header' do
+            expect(response.headers['Content-Type']).to include 'application/activity+json'
+          end
+
+          it 'renders ActivityPub Note object' do
+            json = body_as_json
+            expect(json[:content]).to include status.text
+          end
+        end
+      end
+
+      context 'when status is private' do
+        let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+        context 'when user is authorized to see it' do
+          before do
+            remote_account.follow!(account)
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns no Cache-Control header' do
+              expect(response.headers).to_not include 'Cache-Control'
+            end
+
+            it 'renders status' do
+              expect(response).to render_template(:show)
+              expect(response.body).to include status.text
+            end
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns Content-Type header' do
+              expect(response.headers['Content-Type']).to include 'application/activity+json'
+            end
+
+            it 'renders ActivityPub Note object' do
+              json = body_as_json
+              expect(json[:content]).to include status.text
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          before do
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
+      end
+
+      context 'when status is direct' do
+        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
+
+        context 'when user is authorized to see it' do
+          before do
+            Fabricate(:mention, account: remote_account, status: status)
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns no Cache-Control header' do
+              expect(response.headers).to_not include 'Cache-Control'
+            end
+
+            it 'renders status' do
+              expect(response).to render_template(:show)
+              expect(response.body).to include status.text
+            end
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns Content-Type header' do
+              expect(response.headers['Content-Type']).to include 'application/activity+json'
+            end
+
+            it 'renders ActivityPub Note object' do
+              json = body_as_json
+              expect(json[:content]).to include status.text
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          before do
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
+      end
+    end
+  end
+
+  describe 'GET #activity' do
+    let(:account) { Fabricate(:account) }
+    let(:status)  { Fabricate(:status, account: account) }
+
+    context 'when account is suspended' do
+      let(:account) { Fabricate(:account, suspended: true) }
+
+      before do
+        get :activity, params: { account_username: account.username, id: status.id }
+      end
+
+      it 'returns http gone' do
+        expect(response).to have_http_status(410)
+      end
+    end
+
+    context 'when status is public' do
+      pending
+    end
+
+    context 'when status is private' do
+      pending
+    end
+
+    context 'when status is direct' do
+      pending
+    end
+
+    context 'when signed-in' do
+      context 'when status is public' do
+        pending
+      end
+
+      context 'when status is private' do
+        context 'when user is authorized to see it' do
+          pending
+        end
+
+        context 'when user is not authorized to see it' do
+          pending
+        end
+      end
+
+      context 'when status is direct' do
+        context 'when user is authorized to see it' do
+          pending
+        end
+
+        context 'when user is not authorized to see it' do
+          pending
+        end
+      end
+    end
+
+    context 'with signature' do
+      context 'when status is public' do
+        pending
+      end
+
+      context 'when status is private' do
+        context 'when user is authorized to see it' do
+          pending
+        end
+
+        context 'when user is not authorized to see it' do
+          pending
+        end
+      end
+
+      context 'when status is direct' do
+        context 'when user is authorized to see it' do
+          pending
+        end
+
+        context 'when user is not authorized to see it' do
+          pending
+        end
+      end
+    end
+  end
+
+  describe 'GET #embed' do
+    let(:account) { Fabricate(:account) }
+    let(:status)  { Fabricate(:status, account: account) }
+
+    context 'when account is suspended' do
+      let(:account) { Fabricate(:account, suspended: true) }
+
+      before do
+        get :embed, params: { account_username: account.username, id: status.id }
+      end
+
+      it 'returns http gone' do
+        expect(response).to have_http_status(410)
+      end
+    end
+
+    context 'when status is a reblog' do
+      let(:original_account) { Fabricate(:account, domain: 'example.com') }
+      let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
+      let(:status) { Fabricate(:status, account: account, reblog: original_status) }
+
+      before do
+        get :embed, params: { account_username: status.account.username, id: status.id }
+      end
+
+      it 'returns http not found' do
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when status is public' do
+      before do
+        get :embed, params: { account_username: status.account.username, id: status.id }
+      end
+
+      it 'returns http success' do
         expect(response).to have_http_status(200)
       end
 
-      it 'renders statuses/show' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
-        expect(response).to render_template 'statuses/show'
+      it 'returns Link header' do
+        expect(response.headers['Link'].to_s).to include 'activity+json'
+      end
+
+      it 'returns Vary header' do
+        expect(response.headers['Vary']).to eq 'Accept'
+      end
+
+      it 'returns public Cache-Control header' do
+        expect(response.headers['Cache-Control']).to include 'public'
+      end
+
+      it 'renders status' do
+        expect(response).to render_template(:embed)
+        expect(response.body).to include status.text
+      end
+    end
+
+    context 'when status is private' do
+      let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+      before do
+        get :embed, params: { account_username: status.account.username, id: status.id }
+      end
+
+      it 'returns http not found' do
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when status is direct' do
+      let(:status) { Fabricate(:status, account: account, visibility: :direct) }
+
+      before do
+        get :embed, params: { account_username: status.account.username, id: status.id }
+      end
+
+      it 'returns http not found' do
+        expect(response).to have_http_status(404)
       end
     end
   end
diff --git a/spec/fabricators/status_pin_fabricator.rb b/spec/fabricators/status_pin_fabricator.rb
index 6a9006c9f..f1f1c05f3 100644
--- a/spec/fabricators/status_pin_fabricator.rb
+++ b/spec/fabricators/status_pin_fabricator.rb
@@ -1,4 +1,4 @@
 Fabricator(:status_pin) do
   account
-  status
+  status { |attrs| Fabricate(:status, account: attrs[:account], visibility: :public) }
 end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 38537da44..02f533287 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -96,20 +96,16 @@ RSpec.describe Status, type: :model do
 
     context 'unless destroyed?' do
       context 'if reblog?' do
-        it 'returns "#{account.acct} shared #{reblog.account.acct}\'s: #{preview}"' do
+        it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do
           reblog = subject.reblog = other
-          preview = subject.text.slice(0, 10).split("\n")[0]
-          expect(subject.title).to(
-            eq "#{account.acct} shared #{reblog.account.acct}'s: #{preview}"
-          )
+          expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}"
         end
       end
 
       context 'unless reblog?' do
-        it 'returns "#{account.acct}: #{preview}"' do
+        it 'returns "New status by #{account.acct}"' do
           subject.reblog = nil
-          preview = subject.text.slice(0, 20).split("\n")[0]
-          expect(subject.title).to eq "#{account.acct}: #{preview}"
+          expect(subject.title).to eq "New status by #{account.acct}"
         end
       end
     end
diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb
index 3af6a0689..ded05ffbc 100644
--- a/spec/services/fetch_resource_service_spec.rb
+++ b/spec/services/fetch_resource_service_spec.rb
@@ -21,7 +21,11 @@ RSpec.describe FetchResourceService, type: :service do
 
     context 'when OpenSSL::SSL::SSLError is raised' do
       before do
-        allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(OpenSSL::SSL::SSLError)
+        request = double()
+        allow(Request).to receive(:new).and_return(request)
+        allow(request).to receive(:add_headers)
+        allow(request).to receive(:on_behalf_of)
+        allow(request).to receive(:perform).and_raise(OpenSSL::SSL::SSLError)
       end
 
       it { is_expected.to be_nil }
@@ -29,7 +33,11 @@ RSpec.describe FetchResourceService, type: :service do
 
     context 'when HTTP::ConnectionError is raised' do
       before do
-        allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(HTTP::ConnectionError)
+        request = double()
+        allow(Request).to receive(:new).and_return(request)
+        allow(request).to receive(:add_headers)
+        allow(request).to receive(:on_behalf_of)
+        allow(request).to receive(:perform).and_raise(HTTP::ConnectionError)
       end
 
       it { is_expected.to be_nil }