Refactor `Cache-Control` and `Vary` definitions (#24347)

main
Eugen Rochko 2023-04-19 16:07:29 +02:00 committed by GitHub
parent 4db8230194
commit e98c86050a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 424 additions and 173 deletions

View File

@ -1224,9 +1224,6 @@ Rails/ActiveRecordCallbacksOrder:
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
- 'app/controllers/health_controller.rb' - '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. # Configuration parameters: Database, Include.
# SupportedDatabases: mysql, postgresql # SupportedDatabases: mysql, postgresql

View File

@ -7,8 +7,9 @@ class AccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
include SignatureAuthentication include SignatureAuthentication
vary_by -> { public_fetch_mode? ? 'Accept' : 'Accept, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } 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_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :whitelist_mode?

View File

@ -7,10 +7,6 @@ class ActivityPub::BaseController < Api::BaseController
private private
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
end
def skip_temporary_suspension_response? def skip_temporary_suspension_response?
false false
end end

View File

@ -4,11 +4,12 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification include SignatureVerification
include AccountOwnedConcern include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_items before_action :set_items
before_action :set_size before_action :set_size
before_action :set_type before_action :set_type
before_action :set_cache_headers
def show def show
expires_in 3.minutes, public: public_fetch_mode? expires_in 3.minutes, public: public_fetch_mode?

View File

@ -4,9 +4,10 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
include SignatureVerification include SignatureVerification
include AccountOwnedConcern include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature! before_action :require_account_signature!
before_action :set_items before_action :set_items
before_action :set_cache_headers
def show def show
expires_in 0, public: false expires_in 0, public: false

View File

@ -6,9 +6,10 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
include SignatureVerification include SignatureVerification
include AccountOwnedConcern include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? }
before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers
def show def show
if page_requested? if page_requested?
@ -16,6 +17,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
else else
expires_in(3.minutes, public: public_fetch_mode?) expires_in(3.minutes, public: public_fetch_mode?)
end end
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
@ -80,8 +82,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def set_account def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end end
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
end
end end

View File

@ -7,9 +7,10 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
DESCENDANTS_LIMIT = 60 DESCENDANTS_LIMIT = 60
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status before_action :set_status
before_action :set_cache_headers
before_action :set_replies before_action :set_replies
def index def index

View File

@ -8,6 +8,8 @@ module Admin
layout 'admin' layout 'admin'
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
after_action :verify_authorized after_action :verify_authorized
private private
@ -16,6 +18,10 @@ module Admin
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
def set_user def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end end

View File

@ -12,7 +12,7 @@ class Api::BaseController < ApplicationController
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :require_not_suspended! before_action :require_not_suspended!
before_action :set_cache_headers before_action :set_cache_control_defaults
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
@ -148,8 +148,8 @@ class Api::BaseController < ApplicationController
doorkeeper_authorize!(*scopes) if doorkeeper_token doorkeeper_authorize!(*scopes) if doorkeeper_token
end end
def set_cache_headers def set_cache_control_defaults
response.headers['Cache-Control'] = 'private, no-store' response.cache_control.replace(private: true, no_store: true)
end end
def disallow_unauthenticated_api_access? def disallow_unauthenticated_api_access?

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::CustomEmojisController < Api::BaseController class Api::V1::CustomEmojisController < Api::BaseController
skip_before_action :set_cache_headers
def index def index
expires_in 3.minutes, public: true expires_in 3.minutes, public: true
render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) } render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) }

View File

@ -3,7 +3,6 @@
class Api::V1::Instances::ActivityController < Api::BaseController class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
def show def show

View File

@ -3,7 +3,6 @@
class Api::V1::Instances::PeersController < Api::BaseController class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
def index def index

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::InstancesController < Api::BaseController class Api::V1::InstancesController < Api::BaseController
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
def show def show

View File

@ -152,6 +152,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end
def set_cache_headers def set_cache_headers
response.headers['Cache-Control'] = 'private, no-store' response.cache_control.replace(private: true, no_store: true)
end end
end end

View File

@ -155,8 +155,16 @@ module CacheConcern
end end
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) 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(':') 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 expires_in = options.delete(:expires_in) || 3.minutes
@ -176,10 +184,6 @@ module CacheConcern
end end
end end
def set_cache_headers
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
end
def cache_collection(raw, klass) def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes) return raw unless klass.respond_to?(:with_includes)

View File

@ -1,18 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomCssController < ApplicationController class CustomCssController < ActionController::Base # rubocop:disable Rails/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
def show def show
expires_in 3.minutes, public: true expires_in 3.minutes, public: true
request.session_options[:skip] = true
render content_type: 'text/css' render content_type: 'text/css'
end end
end end

View File

@ -9,10 +9,15 @@ class Disputes::BaseController < ApplicationController
before_action :set_body_classes before_action :set_body_classes
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_cache_headers
private private
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

View File

@ -2,16 +2,13 @@
class EmojisController < ApplicationController class EmojisController < ApplicationController
before_action :set_emoji before_action :set_emoji
before_action :set_cache_headers
vary_by -> { 'Signature' if authorized_fetch_mode? }
def show def show
respond_to do |format|
format.json do
expires_in 3.minutes, public: true expires_in 3.minutes, public: true
render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
end end
end
end
private private

View File

@ -7,6 +7,7 @@ class Filters::StatusesController < ApplicationController
before_action :set_filter before_action :set_filter
before_action :set_status_filters before_action :set_status_filters
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
PER_PAGE = 20 PER_PAGE = 20
@ -44,4 +45,8 @@ class Filters::StatusesController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

View File

@ -6,6 +6,7 @@ class FiltersController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_filter, only: [:edit, :update, :destroy] before_action :set_filter, only: [:edit, :update, :destroy]
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
def index def index
@filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase)
@ -54,4 +55,8 @@ class FiltersController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

View File

@ -5,8 +5,9 @@ class FollowerAccountsController < ApplicationController
include SignatureVerification include SignatureVerification
include WebAppControllerConcern include WebAppControllerConcern
vary_by -> { public_fetch_mode? ? 'Accept' : 'Accept, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } 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_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :whitelist_mode?

View File

@ -5,8 +5,9 @@ class FollowingAccountsController < ApplicationController
include SignatureVerification include SignatureVerification
include WebAppControllerConcern include WebAppControllerConcern
vary_by -> { public_fetch_mode? ? 'Accept' : 'Accept, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } 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_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :whitelist_mode?

View File

@ -7,6 +7,7 @@ class InvitesController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
def index def index
authorize :invite, :create? authorize :invite, :create?
@ -49,4 +50,8 @@ class InvitesController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

View File

@ -1,9 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class ManifestsController < ApplicationController class ManifestsController < ActionController::Base # rubocop:disable Rails/ApplicationController
skip_before_action :store_current_location
skip_before_action :require_functional!
def show def show
expires_in 3.minutes, public: true expires_in 3.minutes, public: true
render json: InstancePresenter.new, serializer: ManifestSerializer, root: 'instance' render json: InstancePresenter.new, serializer: ManifestSerializer, root: 'instance'

View File

@ -34,6 +34,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
end end
def set_cache_headers def set_cache_headers
response.headers['Cache-Control'] = 'private, no-store' response.cache_control.replace(private: true, no_store: true)
end end
end end

View File

@ -7,6 +7,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
before_action :require_not_suspended!, only: :destroy before_action :require_not_suspended!, only: :destroy
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
skip_before_action :require_functional! skip_before_action :require_functional!
@ -30,4 +31,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
def require_not_suspended! def require_not_suspended!
forbidden if current_account.suspended? forbidden if current_account.suspended?
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

View File

@ -7,6 +7,7 @@ class RelationshipsController < ApplicationController
before_action :set_accounts, only: :show before_action :set_accounts, only: :show
before_action :set_relationships, only: :show before_action :set_relationships, only: :show
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
@ -70,4 +71,8 @@ class RelationshipsController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

View File

@ -14,7 +14,7 @@ class Settings::BaseController < ApplicationController
end end
def set_cache_headers def set_cache_headers
response.headers['Cache-Control'] = 'private, no-store' response.cache_control.replace(private: true, no_store: true)
end end
def require_not_suspended! def require_not_suspended!

View File

@ -6,6 +6,7 @@ class StatusesCleanupController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_policy before_action :set_policy
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
def show; end def show; end
@ -36,4 +37,8 @@ class StatusesCleanupController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

View File

@ -6,11 +6,12 @@ class StatusesController < ApplicationController
include Authorization include Authorization
include AccountOwnedConcern 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 :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status before_action :set_status
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :redirect_to_original, only: :show before_action :redirect_to_original, only: :show
before_action :set_cache_headers
before_action :set_body_classes, only: :embed before_action :set_body_classes, only: :embed
after_action :set_link_headers after_action :set_link_headers

View File

@ -7,6 +7,8 @@ class TagsController < ApplicationController
PAGE_SIZE = 20 PAGE_SIZE = 20
PAGE_SIZE_MAX = 200 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 :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_local before_action :set_local

View File

@ -1,11 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
module WellKnown module WellKnown
class HostMetaController < ActionController::Base class HostMetaController < ActionController::Base # rubocop:disable Rails/ApplicationController
include RoutingHelper include RoutingHelper
before_action { response.headers['Vary'] = 'Accept' }
def show def show
@webfinger_template = "#{webfinger_url}?resource={uri}" @webfinger_template = "#{webfinger_url}?resource={uri}"
expires_in 3.days, public: true expires_in 3.days, public: true

View File

@ -1,11 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
module WellKnown module WellKnown
class NodeInfoController < ActionController::Base class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController
include CacheConcern include CacheConcern
before_action { response.headers['Vary'] = 'Accept' }
def index def index
expires_in 3.days, public: true expires_in 3.days, public: true
render_with_cache json: {}, serializer: NodeInfo::DiscoverySerializer, adapter: NodeInfo::Adapter, expires_in: 3.days, root: 'nodeinfo' render_with_cache json: {}, serializer: NodeInfo::DiscoverySerializer, adapter: NodeInfo::Adapter, expires_in: 3.days, root: 'nodeinfo'

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module WellKnown module WellKnown
class WebfingerController < ActionController::Base class WebfingerController < ActionController::Base # rubocop:disable Rails/ApplicationController
include RoutingHelper include RoutingHelper
before_action :set_account before_action :set_account
@ -34,7 +34,12 @@ module WellKnown
end end
def check_account_suspension 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 end
def bad_request def bad_request
@ -46,9 +51,5 @@ module WellKnown
expires_in(3.minutes, public: true) expires_in(3.minutes, public: true)
head 404 head 404
end end
def gone
head 410
end
end end
end end

View File

@ -43,6 +43,7 @@ require_relative '../lib/chewy/strategy/bypass_with_warning'
require_relative '../lib/webpacker/manifest_extensions' require_relative '../lib/webpacker/manifest_extensions'
require_relative '../lib/webpacker/helper_extensions' require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/rails/engine_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/database_tasks_extensions'
require_relative '../lib/active_record/batches' require_relative '../lib/active_record/batches'
require_relative '../lib/simple_navigation/item_extensions' require_relative '../lib/simple_navigation/item_extensions'

View File

@ -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)

View File

@ -17,6 +17,10 @@ RSpec.describe AccountsController, type: :controller do
expect(session).to be_empty expect(session).to be_empty
end end
it 'returns Vary header' do
expect(response.headers['Vary']).to include 'Accept'
end
it 'returns public Cache-Control header' do it 'returns public Cache-Control header' do
expect(response.headers['Cache-Control']).to include 'public' expect(response.headers['Cache-Control']).to include 'public'
end end

View File

@ -18,6 +18,14 @@ describe Admin::BaseController, type: :controller do
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end 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 it 'renders admin layout as a moderator' do
routes.draw { get 'success' => 'admin/base#success' } routes.draw { get 'success' => 'admin/base#success' }
sign_in(Fabricate(:user, role: UserRole.find_by(name: 'Moderator'))) sign_in(Fabricate(:user, role: UserRole.find_by(name: 'Moderator')))

View File

@ -15,6 +15,12 @@ describe Api::BaseController do
end end
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 describe 'forgery protection' do
before do before do
routes.draw { post 'success' => 'api/base#success' } routes.draw { post 'success' => 'api/base#success' }

View File

@ -17,5 +17,9 @@ RSpec.describe Api::OEmbedController, type: :controller do
it 'returns http success' do it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
end end

View File

@ -33,27 +33,42 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
end end
describe 'GET #edit' do describe 'GET #edit' do
it 'returns http success' do before do
request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
sign_in(Fabricate(:user)) sign_in(Fabricate(:user))
get :edit get :edit
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control header' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'GET #update' do describe 'GET #update' do
it 'returns http success' do let(:user) { Fabricate(:user) }
before do
request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
sign_in(Fabricate(:user), scope: :user) sign_in(user, scope: :user)
post :update post :update
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
context 'when suspended' do context 'when suspended' do
let(:user) { Fabricate(:user, account_attributes: { username: 'test', suspended_at: Time.now.utc }) }
it 'returns http forbidden' do 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) expect(response).to have_http_status(403)
end end
end end

View File

@ -6,9 +6,25 @@ describe CustomCssController do
render_views render_views
describe 'GET #show' do describe 'GET #show' do
it 'returns http success' do before do
get :show get :show
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end 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
end end

View File

@ -18,21 +18,27 @@ describe Filters::StatusesController do
context 'with a signed in user' do context 'with a signed in user' do
context 'with the filter user signed in' do context 'with the filter user signed in' do
before { sign_in(filter.account.user) } before do
sign_in(filter.account.user)
get :index, params: { filter_id: filter }
end
it 'returns http success' do it 'returns http success' do
get :index, params: { filter_id: filter }
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
context 'with another user signed in' do context 'with another user signed in' do
before { sign_in(Fabricate(:user)) } before do
sign_in(Fabricate(:user))
get :index, params: { filter_id: filter }
end
it 'returns http not found' do it 'returns http not found' do
get :index, params: { filter_id: filter }
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end

View File

@ -7,21 +7,28 @@ describe FiltersController do
describe 'GET #index' do describe 'GET #index' do
context 'with signed out user' do context 'with signed out user' do
it 'redirects' do before do
get :index get :index
end
it 'redirects' do
expect(response).to be_redirect expect(response).to be_redirect
end end
end end
context 'with a signed in user' do context 'with a signed in user' do
before { sign_in(Fabricate(:user)) } before do
sign_in(Fabricate(:user))
get :index
end
it 'returns http success' do it 'returns http success' do
get :index
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
end end
end end

View File

@ -5,35 +5,40 @@ require 'rails_helper'
describe InvitesController do describe InvitesController do
render_views render_views
let(:user) { Fabricate(:user) }
before do before do
sign_in user sign_in user
end end
describe 'GET #index' do describe 'GET #index' do
subject { get :index } before do
Fabricate(:invite, user: user)
let(:user) { Fabricate(:user) } end
let!(:invite) { Fabricate(:invite, user: user) }
context 'when everyone can invite' do context 'when everyone can invite' do
before do before do
UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users]) UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users])
get :index
end end
it 'renders index page' do it 'returns http success' do
expect(subject).to render_template :index expect(response).to have_http_status(:success)
expect(assigns(:invites)).to include invite end
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
end end
context 'when not everyone can invite' do context 'when not everyone can invite' do
before do before do
UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users]) UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users])
get :index
end end
it 'returns 403' do it 'returns http forbidden' do
expect(subject).to have_http_status 403 expect(response).to have_http_status(403)
end end
end end
end end
@ -42,8 +47,6 @@ describe InvitesController do
subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } } subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } }
context 'when everyone can invite' do context 'when everyone can invite' do
let(:user) { Fabricate(:user) }
before do before do
UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users]) UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users])
end end
@ -56,26 +59,28 @@ describe InvitesController do
end end
context 'when not everyone can invite' do context 'when not everyone can invite' do
let(:user) { Fabricate(:user) }
before do before do
UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users]) UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users])
end end
it 'returns 403' do it 'returns http forbidden' do
expect(subject).to have_http_status 403 expect(subject).to have_http_status(403)
end end
end end
end end
describe 'DELETE #create' do describe 'DELETE #create' do
subject { delete :destroy, params: { id: invite.id } } let(:invite) { Fabricate(:invite, user: user, expires_at: nil) }
let(:user) { Fabricate(:user) } before do
let!(:invite) { Fabricate(:invite, user: user, expires_at: nil) } delete :destroy, params: { id: invite.id }
end
it 'redirects' do
expect(response).to redirect_to invites_path
end
it 'expires invite' do it 'expires invite' do
expect(subject).to redirect_to invites_path
expect(invite.reload).to be_expired expect(invite.reload).to be_expired
end end
end end

View File

@ -7,11 +7,24 @@ describe ManifestsController do
describe 'GET #show' do describe 'GET #show' do
before do before do
get :show, format: :json get :show
end end
it 'returns http success' do it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end 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
end end

View File

@ -31,6 +31,11 @@ RSpec.describe Oauth::AuthorizationsController, type: :controller do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end 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 it 'gives options to authorize and deny' do
subject subject
expect(response.body).to match(/Authorize/) expect(response.body).to match(/Authorize/)

View File

@ -27,6 +27,11 @@ describe Oauth::AuthorizedApplicationsController do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end 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' include_examples 'stores location for user'
end end

View File

@ -7,42 +7,39 @@ describe RelationshipsController do
let(:user) { Fabricate(:user) } 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 describe 'GET #show' do
subject { get :show, params: { page: 2, relationship: 'followed_by' } } context 'when signed in' do
before do
it 'assigns @accounts' do
Fabricate(:account, domain: 'old').follow!(user.account)
Fabricate(:account, domain: 'recent').follow!(user.account)
sign_in user, scope: :user sign_in user, scope: :user
subject get :show, params: { page: 2, relationship: 'followed_by' }
assigned = assigns(:accounts).per(1).to_a
expect(assigned.size).to eq 1
expect(assigned[0].domain).to eq 'old'
end end
it 'returns http success' do it 'returns http success' do
sign_in user, scope: :user
subject
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
include_examples 'authenticate user' 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
before do
get :show, params: { page: 2, relationship: 'followed_by' }
end
it 'redirects when not signed in' do
expect(response).to redirect_to '/auth/sign_in'
end
end
end end
describe 'PATCH #update' do 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 shared_examples 'redirects back to followers page' do
it '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 sign_in user, scope: :user
subject subject
@ -58,27 +55,36 @@ describe RelationshipsController do
end end
context 'when select parameter is provided' do 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 it 'soft-blocks followers from selected domains' do
poopfeast.follow!(user.account) alice.follow!(user.account)
sign_in user, scope: :user sign_in user, scope: :user
subject subject
expect(poopfeast.following?(user.account)).to be false expect(alice.following?(user.account)).to be false
end end
it 'does not unfollow users from selected domains' do it 'does not unfollow users from selected domains' do
user.account.follow!(poopfeast) user.account.follow!(alice)
sign_in user, scope: :user sign_in user, scope: :user
subject 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 end
include_examples 'authenticate user'
include_examples 'redirects back to followers page' include_examples 'redirects back to followers page'
end end
end end

View File

@ -13,10 +13,17 @@ describe Settings::AliasesController do
end end
describe 'GET #index' do describe 'GET #index' do
it 'returns http success' do before do
get :index get :index
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'POST #create' do describe 'POST #create' do

View File

@ -13,13 +13,17 @@ describe Settings::ApplicationsController do
end end
describe 'GET #index' do describe 'GET #index' do
let!(:other_app) { Fabricate(:application) } before do
Fabricate(:application)
it 'shows apps' do
get :index get :index
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(assigns(:applications)).to include(app) end
expect(assigns(:applications)).to_not include(other_app)
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end end
end end

View File

@ -11,20 +11,27 @@ describe Settings::DeletesController do
before do before do
sign_in user, scope: :user sign_in user, scope: :user
get :show
end end
it 'renders confirmation page' do it 'renders confirmation page' do
get :show
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
context 'when suspended' do context 'when suspended' do
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) } let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
it 'returns http forbidden' do it 'returns http forbidden' do
get :show
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
end end

View File

@ -11,16 +11,16 @@ describe Settings::ExportsController do
before do before do
sign_in user, scope: :user sign_in user, scope: :user
get :show
end end
it 'renders export' do it 'returns http success' do
get :show
export = assigns(:export)
expect(export).to be_instance_of Export
expect(export.account).to eq user.account
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
context 'when not signed in' do context 'when not signed in' do

View File

@ -10,10 +10,17 @@ RSpec.describe Settings::ImportsController, type: :controller do
end end
describe 'GET #show' do describe 'GET #show' do
it 'returns http success' do before do
get :show get :show
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'POST #create' do describe 'POST #create' do

View File

@ -12,9 +12,16 @@ describe Settings::LoginActivitiesController do
end end
describe 'GET #index' do describe 'GET #index' do
it 'returns http success' do before do
get :index get :index
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
end end

View File

@ -12,10 +12,17 @@ describe Settings::Migration::RedirectsController do
end end
describe 'GET #new' do describe 'GET #new' do
it 'returns http success' do before do
get :new get :new
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'POST #create' do describe 'POST #create' do

View File

@ -12,11 +12,17 @@ describe Settings::Preferences::AppearanceController do
end end
describe 'GET #show' do describe 'GET #show' do
it 'returns http success' do before do
get :show get :show
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'PUT #update' do describe 'PUT #update' do

View File

@ -12,10 +12,17 @@ describe Settings::Preferences::NotificationsController do
end end
describe 'GET #show' do describe 'GET #show' do
it 'returns http success' do before do
get :show get :show
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'PUT #update' do describe 'PUT #update' do

View File

@ -12,10 +12,17 @@ describe Settings::Preferences::OtherController do
end end
describe 'GET #show' do describe 'GET #show' do
it 'returns http success' do before do
get :show get :show
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'PUT #update' do describe 'PUT #update' do

View File

@ -13,10 +13,17 @@ RSpec.describe Settings::ProfilesController, type: :controller do
end end
describe 'GET #show' do describe 'GET #show' do
it 'returns http success' do before do
get :show get :show
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'PUT #update' do describe 'PUT #update' do

View File

@ -26,23 +26,25 @@ describe Settings::TwoFactorAuthenticationMethodsController do
describe 'when user has enabled otp' do describe 'when user has enabled otp' do
before do before do
user.update(otp_required_for_login: true) user.update(otp_required_for_login: true)
get :index
end end
it 'returns http success' do it 'returns http success' do
get :index
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'when user has not enabled otp' do describe 'when user has not enabled otp' do
before do before do
user.update(otp_required_for_login: false) user.update(otp_required_for_login: false)
get :index
end end
it 'redirects to enable otp' do it 'redirects to enable otp' do
get :index
expect(response).to redirect_to(settings_otp_authentication_path) expect(response).to redirect_to(settings_otp_authentication_path)
end end
end end

View File

@ -11,19 +11,32 @@ RSpec.describe StatusesCleanupController, type: :controller do
end end
describe 'GET #show' do describe 'GET #show' do
it 'returns http success' do before do
get :show get :show
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns private cache control headers' do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end end
describe 'PUT #update' do 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 } } 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.enabled).to be true
expect(@user.account.statuses_cleanup_policy.keep_direct).to be false expect(@user.account.statuses_cleanup_policy.keep_direct).to be false
expect(@user.account.statuses_cleanup_policy.keep_polls).to be true expect(@user.account.statuses_cleanup_policy.keep_polls).to be true
end end
it 'redirects' do
expect(response).to redirect_to(statuses_cleanup_path)
end
end end
end end

View File

@ -15,6 +15,10 @@ describe StatusesController do
expect(session).to be_empty expect(session).to be_empty
end end
it 'returns Vary header' do
expect(response.headers['Vary']).to include 'Accept'
end
it 'returns public Cache-Control header' do it 'returns public Cache-Control header' do
expect(response.headers['Cache-Control']).to include 'public' expect(response.headers['Cache-Control']).to include 'public'
end end

View File

@ -6,21 +6,50 @@ RSpec.describe TagsController, type: :controller do
render_views render_views
describe 'GET #show' do describe 'GET #show' do
let!(:tag) { Fabricate(:tag, name: 'test') } let(:format) { 'html' }
let!(:local) { Fabricate(:status, tags: [tag], text: 'local #test') } let(:tag) { Fabricate(:tag, name: 'test') }
let!(:remote) { Fabricate(:status, tags: [tag], text: 'remote #test', account: Fabricate(:account, domain: 'remote')) } let(:tag_name) { tag&.name }
let!(:late) { Fabricate(:status, tags: [tag], text: 'late #test') }
before do
get :show, params: { id: tag_name, format: format }
end
context 'when tag exists' do context 'when tag exists' do
context 'when requested as HTML' do
it 'returns http success' do it 'returns http success' do
get :show, params: { id: 'test', max_id: late.id }
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end 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 end
context 'when tag does not exist' do context 'when tag does not exist' do
let(:tag_name) { 'hoge' }
it 'returns http not found' do it 'returns http not found' do
get :show, params: { id: 'none' }
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end