~cytrogen/masto-fe

e98c86050a57e03ef61bd9f6b700fcc0c8b1c860 — Eugen Rochko 2 years ago 4db8230
Refactor `Cache-Control` and `Vary` definitions (#24347)

64 files changed, 424 insertions(+), 173 deletions(-)

M .rubocop_todo.yml
M app/controllers/accounts_controller.rb
M app/controllers/activitypub/base_controller.rb
M app/controllers/activitypub/collections_controller.rb
M app/controllers/activitypub/followers_synchronizations_controller.rb
M app/controllers/activitypub/outboxes_controller.rb
M app/controllers/activitypub/replies_controller.rb
M app/controllers/admin/base_controller.rb
M app/controllers/api/base_controller.rb
M app/controllers/api/v1/custom_emojis_controller.rb
M app/controllers/api/v1/instances/activity_controller.rb
M app/controllers/api/v1/instances/peers_controller.rb
M app/controllers/api/v1/instances_controller.rb
M app/controllers/auth/registrations_controller.rb
M app/controllers/concerns/cache_concern.rb
M app/controllers/custom_css_controller.rb
M app/controllers/disputes/base_controller.rb
M app/controllers/emojis_controller.rb
M app/controllers/filters/statuses_controller.rb
M app/controllers/filters_controller.rb
M app/controllers/follower_accounts_controller.rb
M app/controllers/following_accounts_controller.rb
M app/controllers/invites_controller.rb
M app/controllers/manifests_controller.rb
M app/controllers/oauth/authorizations_controller.rb
M app/controllers/oauth/authorized_applications_controller.rb
M app/controllers/relationships_controller.rb
M app/controllers/settings/base_controller.rb
M app/controllers/statuses_cleanup_controller.rb
M app/controllers/statuses_controller.rb
M app/controllers/tags_controller.rb
M app/controllers/well_known/host_meta_controller.rb
M app/controllers/well_known/nodeinfo_controller.rb
M app/controllers/well_known/webfinger_controller.rb
M config/application.rb
A lib/action_controller/conditional_get_extensions.rb
M spec/controllers/accounts_controller_spec.rb
M spec/controllers/admin/base_controller_spec.rb
M spec/controllers/api/base_controller_spec.rb
M spec/controllers/api/oembed_controller_spec.rb
M spec/controllers/auth/registrations_controller_spec.rb
M spec/controllers/custom_css_controller_spec.rb
M spec/controllers/filters/statuses_controller_spec.rb
M spec/controllers/filters_controller_spec.rb
M spec/controllers/invites_controller_spec.rb
M spec/controllers/manifests_controller_spec.rb
M spec/controllers/oauth/authorizations_controller_spec.rb
M spec/controllers/oauth/authorized_applications_controller_spec.rb
M spec/controllers/relationships_controller_spec.rb
M spec/controllers/settings/aliases_controller_spec.rb
M spec/controllers/settings/applications_controller_spec.rb
M spec/controllers/settings/deletes_controller_spec.rb
M spec/controllers/settings/exports_controller_spec.rb
M spec/controllers/settings/imports_controller_spec.rb
M spec/controllers/settings/login_activities_controller_spec.rb
M spec/controllers/settings/migration/redirects_controller_spec.rb
M spec/controllers/settings/preferences/appearance_controller_spec.rb
M spec/controllers/settings/preferences/notifications_controller_spec.rb
M spec/controllers/settings/preferences/other_controller_spec.rb
M spec/controllers/settings/profiles_controller_spec.rb
M spec/controllers/settings/two_factor_authentication_methods_controller_spec.rb
M spec/controllers/statuses_cleanup_controller_spec.rb
M spec/controllers/statuses_controller_spec.rb
M spec/controllers/tags_controller_spec.rb
M .rubocop_todo.yml => .rubocop_todo.yml +0 -3
@@ 1224,9 1224,6 @@ Rails/ActiveRecordCallbacksOrder:
Rails/ApplicationController:
  Exclude:
    - 'app/controllers/health_controller.rb'
    - 'app/controllers/well_known/host_meta_controller.rb'
    - 'app/controllers/well_known/nodeinfo_controller.rb'
    - 'app/controllers/well_known/webfinger_controller.rb'

# Configuration parameters: Database, Include.
# SupportedDatabases: mysql, postgresql

M app/controllers/accounts_controller.rb => app/controllers/accounts_controller.rb +2 -1
@@ 7,8 7,9 @@ class AccountsController < ApplicationController
  include AccountControllerConcern
  include SignatureAuthentication

  vary_by -> { public_fetch_mode? ? 'Accept' : 'Accept, Signature' }

  before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
  before_action :set_cache_headers

  skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
  skip_before_action :require_functional!, unless: :whitelist_mode?

M app/controllers/activitypub/base_controller.rb => app/controllers/activitypub/base_controller.rb +0 -4
@@ 7,10 7,6 @@ class ActivityPub::BaseController < Api::BaseController

  private

  def set_cache_headers
    response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
  end

  def skip_temporary_suspension_response?
    false
  end

M app/controllers/activitypub/collections_controller.rb => app/controllers/activitypub/collections_controller.rb +2 -1
@@ 4,11 4,12 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
  include SignatureVerification
  include AccountOwnedConcern

  vary_by -> { 'Signature' if authorized_fetch_mode? }

  before_action :require_account_signature!, if: :authorized_fetch_mode?
  before_action :set_items
  before_action :set_size
  before_action :set_type
  before_action :set_cache_headers

  def show
    expires_in 3.minutes, public: public_fetch_mode?

M app/controllers/activitypub/followers_synchronizations_controller.rb => app/controllers/activitypub/followers_synchronizations_controller.rb +2 -1
@@ 4,9 4,10 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
  include SignatureVerification
  include AccountOwnedConcern

  vary_by -> { 'Signature' if authorized_fetch_mode? }

  before_action :require_account_signature!
  before_action :set_items
  before_action :set_cache_headers

  def show
    expires_in 0, public: false

M app/controllers/activitypub/outboxes_controller.rb => app/controllers/activitypub/outboxes_controller.rb +3 -5
@@ 6,9 6,10 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
  include SignatureVerification
  include AccountOwnedConcern

  vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? }

  before_action :require_account_signature!, if: :authorized_fetch_mode?
  before_action :set_statuses
  before_action :set_cache_headers

  def show
    if page_requested?


@@ 16,6 17,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
    else
      expires_in(3.minutes, public: public_fetch_mode?)
    end

    render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
  end



@@ 80,8 82,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
  def set_account
    @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
  end

  def set_cache_headers
    response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
  end
end

M app/controllers/activitypub/replies_controller.rb => app/controllers/activitypub/replies_controller.rb +2 -1
@@ 7,9 7,10 @@ class ActivityPub::RepliesController < ActivityPub::BaseController

  DESCENDANTS_LIMIT = 60

  vary_by -> { 'Signature' if authorized_fetch_mode? }

  before_action :require_account_signature!, if: :authorized_fetch_mode?
  before_action :set_status
  before_action :set_cache_headers
  before_action :set_replies

  def index

M app/controllers/admin/base_controller.rb => app/controllers/admin/base_controller.rb +6 -0
@@ 8,6 8,8 @@ module Admin
    layout 'admin'

    before_action :set_body_classes
    before_action :set_cache_headers

    after_action :verify_authorized

    private


@@ 16,6 18,10 @@ module Admin
      @body_classes = 'admin'
    end

    def set_cache_headers
      response.cache_control.replace(private: true, no_store: true)
    end

    def set_user
      @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
    end

M app/controllers/api/base_controller.rb => app/controllers/api/base_controller.rb +3 -3
@@ 12,7 12,7 @@ class Api::BaseController < ApplicationController

  before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
  before_action :require_not_suspended!
  before_action :set_cache_headers
  before_action :set_cache_control_defaults

  protect_from_forgery with: :null_session



@@ 148,8 148,8 @@ class Api::BaseController < ApplicationController
    doorkeeper_authorize!(*scopes) if doorkeeper_token
  end

  def set_cache_headers
    response.headers['Cache-Control'] = 'private, no-store'
  def set_cache_control_defaults
    response.cache_control.replace(private: true, no_store: true)
  end

  def disallow_unauthenticated_api_access?

M app/controllers/api/v1/custom_emojis_controller.rb => app/controllers/api/v1/custom_emojis_controller.rb +0 -2
@@ 1,8 1,6 @@
# frozen_string_literal: true

class Api::V1::CustomEmojisController < Api::BaseController
  skip_before_action :set_cache_headers

  def index
    expires_in 3.minutes, public: true
    render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) }

M app/controllers/api/v1/instances/activity_controller.rb => app/controllers/api/v1/instances/activity_controller.rb +0 -1
@@ 3,7 3,6 @@
class Api::V1::Instances::ActivityController < Api::BaseController
  before_action :require_enabled_api!

  skip_before_action :set_cache_headers
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?

  def show

M app/controllers/api/v1/instances/peers_controller.rb => app/controllers/api/v1/instances/peers_controller.rb +0 -1
@@ 3,7 3,6 @@
class Api::V1::Instances::PeersController < Api::BaseController
  before_action :require_enabled_api!

  skip_before_action :set_cache_headers
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?

  def index

M app/controllers/api/v1/instances_controller.rb => app/controllers/api/v1/instances_controller.rb +0 -1
@@ 1,7 1,6 @@
# frozen_string_literal: true

class Api::V1::InstancesController < Api::BaseController
  skip_before_action :set_cache_headers
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?

  def show

M app/controllers/auth/registrations_controller.rb => app/controllers/auth/registrations_controller.rb +1 -1
@@ 152,6 152,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
  end

  def set_cache_headers
    response.headers['Cache-Control'] = 'private, no-store'
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/concerns/cache_concern.rb => app/controllers/concerns/cache_concern.rb +9 -5
@@ 155,8 155,16 @@ module CacheConcern
    end
  end

  class_methods do
    def vary_by(value)
      before_action do |controller|
        response.headers['Vary'] = value.respond_to?(:call) ? controller.instance_exec(&value) : value
      end
    end
  end

  def render_with_cache(**options)
    raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given?
    raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given?

    key        = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
    expires_in = options.delete(:expires_in) || 3.minutes


@@ 176,10 184,6 @@ module CacheConcern
    end
  end

  def set_cache_headers
    response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
  end

  def cache_collection(raw, klass)
    return raw unless klass.respond_to?(:with_includes)


M app/controllers/custom_css_controller.rb => app/controllers/custom_css_controller.rb +1 -11
@@ 1,18 1,8 @@
# frozen_string_literal: true

class CustomCssController < ApplicationController
  skip_before_action :store_current_location
  skip_before_action :require_functional!
  skip_before_action :update_user_sign_in
  skip_before_action :set_session_activity

  skip_around_action :set_locale

  before_action :set_cache_headers

class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
  def show
    expires_in 3.minutes, public: true
    request.session_options[:skip] = true
    render content_type: 'text/css'
  end
end

M app/controllers/disputes/base_controller.rb => app/controllers/disputes/base_controller.rb +5 -0
@@ 9,10 9,15 @@ class Disputes::BaseController < ApplicationController

  before_action :set_body_classes
  before_action :authenticate_user!
  before_action :set_cache_headers

  private

  def set_body_classes
    @body_classes = 'admin'
  end

  def set_cache_headers
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/emojis_controller.rb => app/controllers/emojis_controller.rb +4 -7
@@ 2,15 2,12 @@

class EmojisController < ApplicationController
  before_action :set_emoji
  before_action :set_cache_headers

  vary_by -> { 'Signature' if authorized_fetch_mode? }

  def show
    respond_to do |format|
      format.json do
        expires_in 3.minutes, public: true
        render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
      end
    end
    expires_in 3.minutes, public: true
    render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
  end

  private

M app/controllers/filters/statuses_controller.rb => app/controllers/filters/statuses_controller.rb +5 -0
@@ 7,6 7,7 @@ class Filters::StatusesController < ApplicationController
  before_action :set_filter
  before_action :set_status_filters
  before_action :set_body_classes
  before_action :set_cache_headers

  PER_PAGE = 20



@@ 44,4 45,8 @@ class Filters::StatusesController < ApplicationController
  def set_body_classes
    @body_classes = 'admin'
  end

  def set_cache_headers
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/filters_controller.rb => app/controllers/filters_controller.rb +5 -0
@@ 6,6 6,7 @@ class FiltersController < ApplicationController
  before_action :authenticate_user!
  before_action :set_filter, only: [:edit, :update, :destroy]
  before_action :set_body_classes
  before_action :set_cache_headers

  def index
    @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase)


@@ 54,4 55,8 @@ class FiltersController < ApplicationController
  def set_body_classes
    @body_classes = 'admin'
  end

  def set_cache_headers
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/follower_accounts_controller.rb => app/controllers/follower_accounts_controller.rb +2 -1
@@ 5,8 5,9 @@ class FollowerAccountsController < ApplicationController
  include SignatureVerification
  include WebAppControllerConcern

  vary_by -> { public_fetch_mode? ? 'Accept' : 'Accept, Signature' }

  before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
  before_action :set_cache_headers

  skip_around_action :set_locale, if: -> { request.format == :json }
  skip_before_action :require_functional!, unless: :whitelist_mode?

M app/controllers/following_accounts_controller.rb => app/controllers/following_accounts_controller.rb +2 -1
@@ 5,8 5,9 @@ class FollowingAccountsController < ApplicationController
  include SignatureVerification
  include WebAppControllerConcern

  vary_by -> { public_fetch_mode? ? 'Accept' : 'Accept, Signature' }

  before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
  before_action :set_cache_headers

  skip_around_action :set_locale, if: -> { request.format == :json }
  skip_before_action :require_functional!, unless: :whitelist_mode?

M app/controllers/invites_controller.rb => app/controllers/invites_controller.rb +5 -0
@@ 7,6 7,7 @@ class InvitesController < ApplicationController

  before_action :authenticate_user!
  before_action :set_body_classes
  before_action :set_cache_headers

  def index
    authorize :invite, :create?


@@ 49,4 50,8 @@ class InvitesController < ApplicationController
  def set_body_classes
    @body_classes = 'admin'
  end

  def set_cache_headers
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/manifests_controller.rb => app/controllers/manifests_controller.rb +1 -4
@@ 1,9 1,6 @@
# frozen_string_literal: true

class ManifestsController < ApplicationController
  skip_before_action :store_current_location
  skip_before_action :require_functional!

class ManifestsController < ActionController::Base # rubocop:disable Rails/ApplicationController
  def show
    expires_in 3.minutes, public: true
    render json: InstancePresenter.new, serializer: ManifestSerializer, root: 'instance'

M app/controllers/oauth/authorizations_controller.rb => app/controllers/oauth/authorizations_controller.rb +1 -1
@@ 34,6 34,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
  end

  def set_cache_headers
    response.headers['Cache-Control'] = 'private, no-store'
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/oauth/authorized_applications_controller.rb => app/controllers/oauth/authorized_applications_controller.rb +5 -0
@@ 7,6 7,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
  before_action :authenticate_resource_owner!
  before_action :require_not_suspended!, only: :destroy
  before_action :set_body_classes
  before_action :set_cache_headers

  skip_before_action :require_functional!



@@ 30,4 31,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
  def require_not_suspended!
    forbidden if current_account.suspended?
  end

  def set_cache_headers
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/relationships_controller.rb => app/controllers/relationships_controller.rb +5 -0
@@ 7,6 7,7 @@ class RelationshipsController < ApplicationController
  before_action :set_accounts, only: :show
  before_action :set_relationships, only: :show
  before_action :set_body_classes
  before_action :set_cache_headers

  helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?



@@ 70,4 71,8 @@ class RelationshipsController < ApplicationController
  def set_body_classes
    @body_classes = 'admin'
  end

  def set_cache_headers
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/settings/base_controller.rb => app/controllers/settings/base_controller.rb +1 -1
@@ 14,7 14,7 @@ class Settings::BaseController < ApplicationController
  end

  def set_cache_headers
    response.headers['Cache-Control'] = 'private, no-store'
    response.cache_control.replace(private: true, no_store: true)
  end

  def require_not_suspended!

M app/controllers/statuses_cleanup_controller.rb => app/controllers/statuses_cleanup_controller.rb +5 -0
@@ 6,6 6,7 @@ class StatusesCleanupController < ApplicationController
  before_action :authenticate_user!
  before_action :set_policy
  before_action :set_body_classes
  before_action :set_cache_headers

  def show; end



@@ 36,4 37,8 @@ class StatusesCleanupController < ApplicationController
  def set_body_classes
    @body_classes = 'admin'
  end

  def set_cache_headers
    response.cache_control.replace(private: true, no_store: true)
  end
end

M app/controllers/statuses_controller.rb => app/controllers/statuses_controller.rb +2 -1
@@ 6,11 6,12 @@ class StatusesController < ApplicationController
  include Authorization
  include AccountOwnedConcern

  vary_by -> { public_fetch_mode? ? 'Accept' : 'Accept, Signature' }

  before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
  before_action :set_status
  before_action :set_instance_presenter
  before_action :redirect_to_original, only: :show
  before_action :set_cache_headers
  before_action :set_body_classes, only: :embed

  after_action :set_link_headers

M app/controllers/tags_controller.rb => app/controllers/tags_controller.rb +2 -0
@@ 7,6 7,8 @@ class TagsController < ApplicationController
  PAGE_SIZE     = 20
  PAGE_SIZE_MAX = 200

  vary_by -> { public_fetch_mode? ? 'Accept' : 'Accept, Signature' }

  before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
  before_action :authenticate_user!, if: :whitelist_mode?
  before_action :set_local

M app/controllers/well_known/host_meta_controller.rb => app/controllers/well_known/host_meta_controller.rb +1 -3
@@ 1,11 1,9 @@
# frozen_string_literal: true

module WellKnown
  class HostMetaController < ActionController::Base
  class HostMetaController < ActionController::Base # rubocop:disable Rails/ApplicationController
    include RoutingHelper

    before_action { response.headers['Vary'] = 'Accept' }

    def show
      @webfinger_template = "#{webfinger_url}?resource={uri}"
      expires_in 3.days, public: true

M app/controllers/well_known/nodeinfo_controller.rb => app/controllers/well_known/nodeinfo_controller.rb +1 -3
@@ 1,11 1,9 @@
# frozen_string_literal: true

module WellKnown
  class NodeInfoController < ActionController::Base
  class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController
    include CacheConcern

    before_action { response.headers['Vary'] = 'Accept' }

    def index
      expires_in 3.days, public: true
      render_with_cache json: {}, serializer: NodeInfo::DiscoverySerializer, adapter: NodeInfo::Adapter, expires_in: 3.days, root: 'nodeinfo'

M app/controllers/well_known/webfinger_controller.rb => app/controllers/well_known/webfinger_controller.rb +7 -6
@@ 1,7 1,7 @@
# frozen_string_literal: true

module WellKnown
  class WebfingerController < ActionController::Base
  class WebfingerController < ActionController::Base # rubocop:disable Rails/ApplicationController
    include RoutingHelper

    before_action :set_account


@@ 34,7 34,12 @@ module WellKnown
    end

    def check_account_suspension
      expires_in(3.minutes, public: true) && gone if @account.suspended_permanently?
      gone if @account.suspended_permanently?
    end

    def gone
      expires_in(3.minutes, public: true)
      head 410
    end

    def bad_request


@@ 46,9 51,5 @@ module WellKnown
      expires_in(3.minutes, public: true)
      head 404
    end

    def gone
      head 410
    end
  end
end

M config/application.rb => config/application.rb +1 -0
@@ 43,6 43,7 @@ require_relative '../lib/chewy/strategy/bypass_with_warning'
require_relative '../lib/webpacker/manifest_extensions'
require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/rails/engine_extensions'
require_relative '../lib/action_controller/conditional_get_extensions'
require_relative '../lib/active_record/database_tasks_extensions'
require_relative '../lib/active_record/batches'
require_relative '../lib/simple_navigation/item_extensions'

A lib/action_controller/conditional_get_extensions.rb => lib/action_controller/conditional_get_extensions.rb +15 -0
@@ 0,0 1,15 @@
# frozen_string_literal: true

module ActionController
  module ConditionalGetExtensions
    def expires_in(*)
      # This backports a fix from Rails 7 so that a more private Cache-Control
      # can be overriden by calling expires_in on a specific controller action
      response.cache_control.delete(:no_store)

      super
    end
  end
end

ActionController::ConditionalGet.prepend(ActionController::ConditionalGetExtensions)

M spec/controllers/accounts_controller_spec.rb => spec/controllers/accounts_controller_spec.rb +4 -0
@@ 17,6 17,10 @@ RSpec.describe AccountsController, type: :controller do
      expect(session).to be_empty
    end

    it 'returns Vary header' do
      expect(response.headers['Vary']).to include 'Accept'
    end

    it 'returns public Cache-Control header' do
      expect(response.headers['Cache-Control']).to include 'public'
    end

M spec/controllers/admin/base_controller_spec.rb => spec/controllers/admin/base_controller_spec.rb +8 -0
@@ 18,6 18,14 @@ describe Admin::BaseController, type: :controller do
    expect(response).to have_http_status(403)
  end

  it 'returns private cache control headers' do
    routes.draw { get 'success' => 'admin/base#success' }
    sign_in(Fabricate(:user, role: UserRole.find_by(name: 'Moderator')))
    get :success

    expect(response.headers['Cache-Control']).to include('private, no-store')
  end

  it 'renders admin layout as a moderator' do
    routes.draw { get 'success' => 'admin/base#success' }
    sign_in(Fabricate(:user, role: UserRole.find_by(name: 'Moderator')))

M spec/controllers/api/base_controller_spec.rb => spec/controllers/api/base_controller_spec.rb +6 -0
@@ 15,6 15,12 @@ describe Api::BaseController do
    end
  end

  it 'returns private cache control headers by default' do
    routes.draw { get 'success' => 'api/base#success' }
    get :success
    expect(response.headers['Cache-Control']).to include('private, no-store')
  end

  describe 'forgery protection' do
    before do
      routes.draw { post 'success' => 'api/base#success' }

M spec/controllers/api/oembed_controller_spec.rb => spec/controllers/api/oembed_controller_spec.rb +4 -0
@@ 17,5 17,9 @@ RSpec.describe Api::OEmbedController, type: :controller do
    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end
end

M spec/controllers/auth/registrations_controller_spec.rb => spec/controllers/auth/registrations_controller_spec.rb +21 -6
@@ 33,27 33,42 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
  end

  describe 'GET #edit' do
    it 'returns http success' do
    before do
      request.env['devise.mapping'] = Devise.mappings[:user]
      sign_in(Fabricate(:user))
      get :edit
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control header' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'GET #update' do
    it 'returns http success' do
    let(:user) { Fabricate(:user) }

    before do
      request.env['devise.mapping'] = Devise.mappings[:user]
      sign_in(Fabricate(:user), scope: :user)
      sign_in(user, scope: :user)
      post :update
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end

    context 'when suspended' do
      let(:user) { Fabricate(:user, account_attributes: { username: 'test', suspended_at: Time.now.utc }) }

      it 'returns http forbidden' do
        request.env['devise.mapping'] = Devise.mappings[:user]
        sign_in(Fabricate(:user, account_attributes: { username: 'test', suspended_at: Time.now.utc }), scope: :user)
        post :update
        expect(response).to have_http_status(403)
      end
    end

M spec/controllers/custom_css_controller_spec.rb => spec/controllers/custom_css_controller_spec.rb +17 -1
@@ 6,9 6,25 @@ describe CustomCssController do
  render_views

  describe 'GET #show' do
    it 'returns http success' do
    before do
      get :show
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns public cache control header' do
      expect(response.headers['Cache-Control']).to include('public')
    end

    it 'does not set cookies' do
      expect(response.cookies).to be_empty
      expect(response.headers['Set-Cookies']).to be_nil
    end

    it 'does not set sessions' do
      expect(session).to be_empty
    end
  end
end

M spec/controllers/filters/statuses_controller_spec.rb => spec/controllers/filters/statuses_controller_spec.rb +12 -6
@@ 18,21 18,27 @@ describe Filters::StatusesController do

    context 'with a signed in user' do
      context 'with the filter user signed in' do
        before { sign_in(filter.account.user) }

        it 'returns http success' do
        before do
          sign_in(filter.account.user)
          get :index, params: { filter_id: filter }
        end

        it 'returns http success' do
          expect(response).to have_http_status(200)
        end

        it 'returns private cache control headers' do
          expect(response.headers['Cache-Control']).to include('private, no-store')
        end
      end

      context 'with another user signed in' do
        before { sign_in(Fabricate(:user)) }

        it 'returns http not found' do
        before do
          sign_in(Fabricate(:user))
          get :index, params: { filter_id: filter }
        end

        it 'returns http not found' do
          expect(response).to have_http_status(404)
        end
      end

M spec/controllers/filters_controller_spec.rb => spec/controllers/filters_controller_spec.rb +11 -4
@@ 7,21 7,28 @@ describe FiltersController do

  describe 'GET #index' do
    context 'with signed out user' do
      it 'redirects' do
      before do
        get :index
      end

      it 'redirects' do
        expect(response).to be_redirect
      end
    end

    context 'with a signed in user' do
      before { sign_in(Fabricate(:user)) }

      it 'returns http success' do
      before do
        sign_in(Fabricate(:user))
        get :index
      end

      it 'returns http success' do
        expect(response).to have_http_status(200)
      end

      it 'returns private cache control headers' do
        expect(response.headers['Cache-Control']).to include('private, no-store')
      end
    end
  end
end

M spec/controllers/invites_controller_spec.rb => spec/controllers/invites_controller_spec.rb +25 -20
@@ 5,35 5,40 @@ require 'rails_helper'
describe InvitesController do
  render_views

  let(:user) { Fabricate(:user) }

  before do
    sign_in user
  end

  describe 'GET #index' do
    subject { get :index }

    let(:user) { Fabricate(:user) }
    let!(:invite) { Fabricate(:invite, user: user) }
    before do
      Fabricate(:invite, user: user)
    end

    context 'when everyone can invite' do
      before do
        UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users])
        get :index
      end

      it 'returns http success' do
        expect(response).to have_http_status(:success)
      end

      it 'renders index page' do
        expect(subject).to render_template :index
        expect(assigns(:invites)).to include invite
        expect(assigns(:invites).count).to eq 1
      it 'returns private cache control headers' do
        expect(response.headers['Cache-Control']).to include('private, no-store')
      end
    end

    context 'when not everyone can invite' do
      before do
        UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users])
        get :index
      end

      it 'returns 403' do
        expect(subject).to have_http_status 403
      it 'returns http forbidden' do
        expect(response).to have_http_status(403)
      end
    end
  end


@@ 42,8 47,6 @@ describe InvitesController do
    subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } }

    context 'when everyone can invite' do
      let(:user) { Fabricate(:user) }

      before do
        UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users])
      end


@@ 56,26 59,28 @@ describe InvitesController do
    end

    context 'when not everyone can invite' do
      let(:user) { Fabricate(:user) }

      before do
        UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users])
      end

      it 'returns 403' do
        expect(subject).to have_http_status 403
      it 'returns http forbidden' do
        expect(subject).to have_http_status(403)
      end
    end
  end

  describe 'DELETE #create' do
    subject { delete :destroy, params: { id: invite.id } }
    let(:invite) { Fabricate(:invite, user: user, expires_at: nil) }

    let(:user) { Fabricate(:user) }
    let!(:invite) { Fabricate(:invite, user: user, expires_at: nil) }
    before do
      delete :destroy, params: { id: invite.id }
    end

    it 'redirects' do
      expect(response).to redirect_to invites_path
    end

    it 'expires invite' do
      expect(subject).to redirect_to invites_path
      expect(invite.reload).to be_expired
    end
  end

M spec/controllers/manifests_controller_spec.rb => spec/controllers/manifests_controller_spec.rb +14 -1
@@ 7,11 7,24 @@ describe ManifestsController do

  describe 'GET #show' do
    before do
      get :show, format: :json
      get :show
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns public cache control header' do
      expect(response.headers['Cache-Control']).to include('public')
    end

    it 'does not set cookies' do
      expect(response.cookies).to be_empty
      expect(response.headers['Set-Cookies']).to be_nil
    end

    it 'does not set sessions' do
      expect(session).to be_empty
    end
  end
end

M spec/controllers/oauth/authorizations_controller_spec.rb => spec/controllers/oauth/authorizations_controller_spec.rb +5 -0
@@ 31,6 31,11 @@ RSpec.describe Oauth::AuthorizationsController, type: :controller do
        expect(response).to have_http_status(200)
      end

      it 'returns private cache control headers' do
        subject
        expect(response.headers['Cache-Control']).to include('private, no-store')
      end

      it 'gives options to authorize and deny' do
        subject
        expect(response.body).to match(/Authorize/)

M spec/controllers/oauth/authorized_applications_controller_spec.rb => spec/controllers/oauth/authorized_applications_controller_spec.rb +5 -0
@@ 27,6 27,11 @@ describe Oauth::AuthorizedApplicationsController do
        expect(response).to have_http_status(200)
      end

      it 'returns private cache control headers' do
        subject
        expect(response.headers['Cache-Control']).to include('private, no-store')
      end

      include_examples 'stores location for user'
    end


M spec/controllers/relationships_controller_spec.rb => spec/controllers/relationships_controller_spec.rb +36 -30
@@ 7,42 7,39 @@ describe RelationshipsController do

  let(:user) { Fabricate(:user) }

  shared_examples 'authenticate user' do
    it 'redirects when not signed in' do
      expect(subject).to redirect_to '/auth/sign_in'
    end
  end

  describe 'GET #show' do
    subject { get :show, params: { page: 2, relationship: 'followed_by' } }

    it 'assigns @accounts' do
      Fabricate(:account, domain: 'old').follow!(user.account)
      Fabricate(:account, domain: 'recent').follow!(user.account)
    context 'when signed in' do
      before do
        sign_in user, scope: :user
        get :show, params: { page: 2, relationship: 'followed_by' }
      end

      sign_in user, scope: :user
      subject
      it 'returns http success' do
        expect(response).to have_http_status(200)
      end

      assigned = assigns(:accounts).per(1).to_a
      expect(assigned.size).to eq 1
      expect(assigned[0].domain).to eq 'old'
      it 'returns private cache control headers' do
        expect(response.headers['Cache-Control']).to include('private, no-store')
      end
    end

    it 'returns http success' do
      sign_in user, scope: :user
      subject
      expect(response).to have_http_status(200)
    end
    context 'when not signed in' do
      before do
        get :show, params: { page: 2, relationship: 'followed_by' }
      end

    include_examples 'authenticate user'
      it 'redirects when not signed in' do
        expect(response).to redirect_to '/auth/sign_in'
      end
    end
  end

  describe 'PATCH #update' do
    let(:poopfeast) { Fabricate(:account, username: 'poopfeast', domain: 'example.com') }
    let(:alice) { Fabricate(:account, username: 'alice', domain: 'example.com') }

    shared_examples 'redirects back to followers page' do
      it 'redirects back to followers page' do
        poopfeast.follow!(user.account)
        alice.follow!(user.account)

        sign_in user, scope: :user
        subject


@@ 58,27 55,36 @@ describe RelationshipsController do
    end

    context 'when select parameter is provided' do
      subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, remove_domains_from_followers: '' } }
      subject { patch :update, params: { form_account_batch: { account_ids: [alice.id] }, remove_domains_from_followers: '' } }

      it 'soft-blocks followers from selected domains' do
        poopfeast.follow!(user.account)
        alice.follow!(user.account)

        sign_in user, scope: :user
        subject

        expect(poopfeast.following?(user.account)).to be false
        expect(alice.following?(user.account)).to be false
      end

      it 'does not unfollow users from selected domains' do
        user.account.follow!(poopfeast)
        user.account.follow!(alice)

        sign_in user, scope: :user
        subject

        expect(user.account.following?(poopfeast)).to be true
        expect(user.account.following?(alice)).to be true
      end

      context 'when not signed in' do
        before do
          subject
        end

        it 'redirects when not signed in' do
          expect(response).to redirect_to '/auth/sign_in'
        end
      end

      include_examples 'authenticate user'
      include_examples 'redirects back to followers page'
    end
  end

M spec/controllers/settings/aliases_controller_spec.rb => spec/controllers/settings/aliases_controller_spec.rb +8 -1
@@ 13,10 13,17 @@ describe Settings::AliasesController do
  end

  describe 'GET #index' do
    it 'returns http success' do
    before do
      get :index
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'POST #create' do

M spec/controllers/settings/applications_controller_spec.rb => spec/controllers/settings/applications_controller_spec.rb +9 -5
@@ 13,13 13,17 @@ describe Settings::ApplicationsController do
  end

  describe 'GET #index' do
    let!(:other_app) { Fabricate(:application) }

    it 'shows apps' do
    before do
      Fabricate(:application)
      get :index
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
      expect(assigns(:applications)).to include(app)
      expect(assigns(:applications)).to_not include(other_app)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end


M spec/controllers/settings/deletes_controller_spec.rb => spec/controllers/settings/deletes_controller_spec.rb +9 -2
@@ 11,20 11,27 @@ describe Settings::DeletesController do

      before do
        sign_in user, scope: :user
        get :show
      end

      it 'renders confirmation page' do
        get :show
        expect(response).to have_http_status(200)
      end

      it 'returns private cache control headers' do
        expect(response.headers['Cache-Control']).to include('private, no-store')
      end

      context 'when suspended' do
        let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }

        it 'returns http forbidden' do
          get :show
          expect(response).to have_http_status(403)
        end

        it 'returns private cache control headers' do
          expect(response.headers['Cache-Control']).to include('private, no-store')
        end
      end
    end


M spec/controllers/settings/exports_controller_spec.rb => spec/controllers/settings/exports_controller_spec.rb +6 -6
@@ 11,16 11,16 @@ describe Settings::ExportsController do

      before do
        sign_in user, scope: :user
      end

      it 'renders export' do
        get :show
      end

        export = assigns(:export)
        expect(export).to be_instance_of Export
        expect(export.account).to eq user.account
      it 'returns http success' do
        expect(response).to have_http_status(200)
      end

      it 'returns private cache control headers' do
        expect(response.headers['Cache-Control']).to include('private, no-store')
      end
    end

    context 'when not signed in' do

M spec/controllers/settings/imports_controller_spec.rb => spec/controllers/settings/imports_controller_spec.rb +8 -1
@@ 10,10 10,17 @@ RSpec.describe Settings::ImportsController, type: :controller do
  end

  describe 'GET #show' do
    it 'returns http success' do
    before do
      get :show
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'POST #create' do

M spec/controllers/settings/login_activities_controller_spec.rb => spec/controllers/settings/login_activities_controller_spec.rb +8 -1
@@ 12,9 12,16 @@ describe Settings::LoginActivitiesController do
  end

  describe 'GET #index' do
    it 'returns http success' do
    before do
      get :index
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end
end

M spec/controllers/settings/migration/redirects_controller_spec.rb => spec/controllers/settings/migration/redirects_controller_spec.rb +8 -1
@@ 12,10 12,17 @@ describe Settings::Migration::RedirectsController do
  end

  describe 'GET #new' do
    it 'returns http success' do
    before do
      get :new
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'POST #create' do

M spec/controllers/settings/preferences/appearance_controller_spec.rb => spec/controllers/settings/preferences/appearance_controller_spec.rb +7 -1
@@ 12,11 12,17 @@ describe Settings::Preferences::AppearanceController do
  end

  describe 'GET #show' do
    it 'returns http success' do
    before do
      get :show
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'PUT #update' do

M spec/controllers/settings/preferences/notifications_controller_spec.rb => spec/controllers/settings/preferences/notifications_controller_spec.rb +8 -1
@@ 12,10 12,17 @@ describe Settings::Preferences::NotificationsController do
  end

  describe 'GET #show' do
    it 'returns http success' do
    before do
      get :show
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'PUT #update' do

M spec/controllers/settings/preferences/other_controller_spec.rb => spec/controllers/settings/preferences/other_controller_spec.rb +8 -1
@@ 12,10 12,17 @@ describe Settings::Preferences::OtherController do
  end

  describe 'GET #show' do
    it 'returns http success' do
    before do
      get :show
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'PUT #update' do

M spec/controllers/settings/profiles_controller_spec.rb => spec/controllers/settings/profiles_controller_spec.rb +8 -1
@@ 13,10 13,17 @@ RSpec.describe Settings::ProfilesController, type: :controller do
  end

  describe 'GET #show' do
    it 'returns http success' do
    before do
      get :show
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'PUT #update' do

M spec/controllers/settings/two_factor_authentication_methods_controller_spec.rb => spec/controllers/settings/two_factor_authentication_methods_controller_spec.rb +6 -4
@@ 26,23 26,25 @@ describe Settings::TwoFactorAuthenticationMethodsController do
      describe 'when user has enabled otp' do
        before do
          user.update(otp_required_for_login: true)
          get :index
        end

        it 'returns http success' do
          get :index

          expect(response).to have_http_status(200)
        end

        it 'returns private cache control headers' do
          expect(response.headers['Cache-Control']).to include('private, no-store')
        end
      end

      describe 'when user has not enabled otp' do
        before do
          user.update(otp_required_for_login: false)
          get :index
        end

        it 'redirects to enable otp' do
          get :index

          expect(response).to redirect_to(settings_otp_authentication_path)
        end
      end

M spec/controllers/statuses_cleanup_controller_spec.rb => spec/controllers/statuses_cleanup_controller_spec.rb +16 -3
@@ 11,19 11,32 @@ RSpec.describe StatusesCleanupController, type: :controller do
  end

  describe 'GET #show' do
    it 'returns http success' do
    before do
      get :show
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'returns private cache control headers' do
      expect(response.headers['Cache-Control']).to include('private, no-store')
    end
  end

  describe 'PUT #update' do
    it 'updates the account status cleanup policy' do
    before do
      put :update, params: { account_statuses_cleanup_policy: { enabled: true, min_status_age: 2.weeks.seconds, keep_direct: false, keep_polls: true } }
      expect(response).to redirect_to(statuses_cleanup_path)
    end

    it 'updates the account status cleanup policy' do
      expect(@user.account.statuses_cleanup_policy.enabled).to be true
      expect(@user.account.statuses_cleanup_policy.keep_direct).to be false
      expect(@user.account.statuses_cleanup_policy.keep_polls).to be true
    end

    it 'redirects' do
      expect(response).to redirect_to(statuses_cleanup_path)
    end
  end
end

M spec/controllers/statuses_controller_spec.rb => spec/controllers/statuses_controller_spec.rb +4 -0
@@ 15,6 15,10 @@ describe StatusesController do
      expect(session).to be_empty
    end

    it 'returns Vary header' do
      expect(response.headers['Vary']).to include 'Accept'
    end

    it 'returns public Cache-Control header' do
      expect(response.headers['Cache-Control']).to include 'public'
    end

M spec/controllers/tags_controller_spec.rb => spec/controllers/tags_controller_spec.rb +37 -8
@@ 6,21 6,50 @@ RSpec.describe TagsController, type: :controller do
  render_views

  describe 'GET #show' do
    let!(:tag)     { Fabricate(:tag, name: 'test') }
    let!(:local)   { Fabricate(:status, tags: [tag], text: 'local #test') }
    let!(:remote)  { Fabricate(:status, tags: [tag], text: 'remote #test', account: Fabricate(:account, domain: 'remote')) }
    let!(:late)    { Fabricate(:status, tags: [tag], text: 'late #test') }
    let(:format) { 'html' }
    let(:tag) { Fabricate(:tag, name: 'test') }
    let(:tag_name) { tag&.name }

    before do
      get :show, params: { id: tag_name, format: format }
    end

    context 'when tag exists' do
      it 'returns http success' do
        get :show, params: { id: 'test', max_id: late.id }
        expect(response).to have_http_status(200)
      context 'when requested as HTML' do
        it 'returns http success' do
          expect(response).to have_http_status(200)
        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
      end

      context 'when requested as JSON' do
        let(:format) { 'json' }

        it 'returns http success' do
          expect(response).to have_http_status(200)
        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
      end
    end

    context 'when tag does not exist' do
      let(:tag_name) { 'hoge' }

      it 'returns http not found' do
        get :show, params: { id: 'none' }
        expect(response).to have_http_status(404)
      end
    end