123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632 |
- # frozen_string_literal: true
- require 'rails_helper'
- module TestEndpoints
- # Endpoints that do not include authorization-dependent results
- # and should be cacheable no matter what.
- ALWAYS_CACHED = %w(
- /.well-known/host-meta
- /.well-known/nodeinfo
- /nodeinfo/2.0
- /manifest
- /custom.css
- /actor
- /api/v1/instance/extended_description
- /api/v1/instance/rules
- /api/v1/instance/peers
- /api/v1/instance
- /api/v2/instance
- ).freeze
- # Endpoints that should be cachable when accessed anonymously but have a Vary
- # on Cookie to prevent logged-in users from getting values from logged-out cache.
- COOKIE_DEPENDENT_CACHABLE = %w(
- /
- /explore
- /public
- /about
- /privacy-policy
- /directory
- /@alice
- /@alice/110224538612341312
- /deck/home
- ).freeze
- # Endpoints that should be cachable when accessed anonymously but have a Vary
- # on Authorization to prevent logged-in users from getting values from logged-out cache.
- AUTHORIZATION_DEPENDENT_CACHABLE = %w(
- /api/v1/accounts/lookup?acct=alice
- /api/v1/statuses/110224538612341312
- /api/v1/statuses/110224538612341312/context
- /api/v1/polls/123456789
- /api/v1/trends/statuses
- /api/v1/directory
- ).freeze
- # Private status that should only be returned with to a valid signature from
- # a specific user.
- # Should never be cached.
- REQUIRE_SIGNATURE = %w(
- /users/alice/statuses/110224538643211312
- ).freeze
- # Pages only available to logged-in users.
- # Should never be cached.
- REQUIRE_LOGIN = %w(
- /settings/preferences/appearance
- /settings/profile
- /settings/featured_tags
- /settings/export
- /relationships
- /filters
- /statuses_cleanup
- /auth/edit
- /oauth/authorized_applications
- /admin/dashboard
- ).freeze
- # API endpoints only available to logged-in users.
- # Should never be cached.
- REQUIRE_TOKEN = %w(
- /api/v1/announcements
- /api/v1/timelines/home
- /api/v1/notifications
- /api/v1/bookmarks
- /api/v1/favourites
- /api/v1/follow_requests
- /api/v1/conversations
- /api/v1/statuses/110224538643211312
- /api/v1/statuses/110224538643211312/context
- /api/v1/lists
- /api/v2/filters
- ).freeze
- # Pages that are only shown to logged-out users, and should never get cached
- # because of CSRF protection.
- REQUIRE_LOGGED_OUT = %w(
- /invite/abcdef
- /auth/sign_in
- /auth/sign_up
- /auth/password/new
- /auth/confirmation/new
- ).freeze
- # Non-exhaustive list of endpoints that feature language-dependent results
- # and thus need to have a Vary on Accept-Language
- LANGUAGE_DEPENDENT = %w(
- /
- /explore
- /about
- /api/v1/trends/statuses
- ).freeze
- module AuthorizedFetch
- # Endpoints that require a signature with AUTHORIZED_FETCH and LIMITED_FEDERATION_MODE
- # and thus should not be cached in those modes.
- REQUIRE_SIGNATURE = %w(
- /users/alice
- ).freeze
- end
- module DisabledAnonymousAPI
- # Endpoints that require a signature with DISALLOW_UNAUTHENTICATED_API_ACCESS
- # and thus should not be cached in this mode.
- REQUIRE_TOKEN = %w(
- /api/v1/custom_emojis
- ).freeze
- end
- end
- RSpec.describe 'Caching behavior' do
- shared_examples 'cachable response' do |http_success: false|
- it 'does not set cookies or set public cache control', :aggregate_failures do
- expect(response.cookies).to be_empty
- # expect(response.cache_control[:max_age]&.to_i).to be_positive
- expect(response.cache_control[:public]).to be_truthy
- expect(response.cache_control[:private]).to be_falsy
- expect(response.cache_control[:no_store]).to be_falsy
- expect(response.cache_control[:no_cache]).to be_falsy
- expect(response).to have_http_status(200) if http_success
- end
- end
- shared_examples 'non-cacheable response' do |http_success: false|
- it 'sets private cache control' do
- expect(response.cache_control[:private]).to be_truthy
- expect(response.cache_control[:no_store]).to be_truthy
- expect(response).to have_http_status(200) if http_success
- end
- end
- shared_examples 'non-cacheable error' do
- it 'does not return HTTP success and does not have cache headers', :aggregate_failures do
- expect(response).to_not have_http_status(200)
- expect(response.cache_control[:public]).to be_falsy
- end
- end
- shared_examples 'language-dependent' do
- it 'has a Vary on Accept-Language' do
- expect(response_vary_headers).to include('accept-language')
- end
- end
- # Enable CSRF protection like it is in production, as it can cause cookies
- # to be set and thus mess with cache.
- around do |example|
- old = ActionController::Base.allow_forgery_protection
- ActionController::Base.allow_forgery_protection = true
- example.run
- ActionController::Base.allow_forgery_protection = old
- end
- let(:alice) { Account.find_by(username: 'alice') }
- let(:user) { User.find_by(email: 'user@host.example') }
- let(:token) { Doorkeeper::AccessToken.find_by(resource_owner_id: user.id) }
- before_all do
- alice = Fabricate(:account, username: 'alice')
- user = Fabricate(:user, email: 'user@host.example', role: UserRole.find_by(name: 'Moderator'))
- status = Fabricate(:status, account: alice, id: 110_224_538_612_341_312)
- Fabricate(:status, account: alice, id: 110_224_538_643_211_312, visibility: :private)
- Fabricate(:invite, code: 'abcdef')
- Fabricate(:poll, status: status, account: alice, id: 123_456_789)
- Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read')
- user.account.follow!(alice)
- end
- context 'when anonymously accessed' do
- describe '/users/alice' do
- it 'redirects with proper cache header', :aggregate_failures do
- get '/users/alice'
- expect(response).to redirect_to('/@alice')
- expect(response_vary_headers).to include('accept')
- end
- end
- TestEndpoints::ALWAYS_CACHED.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'cachable response'
- it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
- end
- end
- TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'cachable response'
- it 'has a Vary on Cookie' do
- expect(response_vary_headers).to include('cookie')
- end
- it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
- end
- end
- TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'cachable response'
- it 'has a Vary on Authorization' do
- expect(response_vary_headers).to include('authorization')
- end
- it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
- end
- end
- TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'non-cacheable response'
- end
- end
- (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::REQUIRE_LOGIN + TestEndpoints::REQUIRE_TOKEN).each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'non-cacheable error'
- end
- end
- describe '/api/v1/instance/domain_blocks' do
- before do
- Setting.show_domain_blocks = show_domain_blocks
- get '/api/v1/instance/domain_blocks'
- end
- context 'when set to be publicly-available' do
- let(:show_domain_blocks) { 'all' }
- it_behaves_like 'cachable response'
- end
- context 'when allowed for local users only' do
- let(:show_domain_blocks) { 'users' }
- it_behaves_like 'non-cacheable error'
- end
- context 'when disabled' do
- let(:show_domain_blocks) { 'disabled' }
- it_behaves_like 'non-cacheable error'
- end
- end
- end
- context 'when logged in' do
- before do
- sign_in user, scope: :user
- # Unfortunately, devise's `sign_in` helper causes the `session` to be
- # loaded in the next request regardless of whether it's actually accessed
- # by the client code.
- #
- # So, we make an extra query to clear issue a session cookie instead.
- #
- # A less resource-intensive way to deal with that would be to generate the
- # session cookie manually, but this seems pretty involved.
- get '/'
- end
- TestEndpoints::ALWAYS_CACHED.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'cachable response'
- it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
- end
- end
- TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'non-cacheable response'
- it 'has a Vary on Cookie' do
- expect(response_vary_headers).to include('cookie')
- end
- end
- end
- TestEndpoints::REQUIRE_LOGIN.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'non-cacheable response', http_success: true
- end
- end
- TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'non-cacheable error'
- end
- end
- end
- context 'with an auth token' do
- TestEndpoints::ALWAYS_CACHED.each do |endpoint|
- describe endpoint do
- before do
- get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
- end
- it_behaves_like 'cachable response'
- it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
- end
- end
- TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
- describe endpoint do
- before do
- get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
- end
- it_behaves_like 'non-cacheable response'
- it 'has a Vary on Authorization' do
- expect(response_vary_headers).to include('authorization')
- end
- end
- end
- (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN).each do |endpoint|
- describe endpoint do
- before do
- get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
- end
- it_behaves_like 'non-cacheable response', http_success: true
- end
- end
- describe '/api/v1/instance/domain_blocks' do
- before do
- Setting.show_domain_blocks = show_domain_blocks
- get '/api/v1/instance/domain_blocks', headers: { 'Authorization' => "Bearer #{token.token}" }
- end
- context 'when set to be publicly-available' do
- let(:show_domain_blocks) { 'all' }
- it_behaves_like 'cachable response'
- end
- context 'when allowed for local users only' do
- let(:show_domain_blocks) { 'users' }
- it_behaves_like 'non-cacheable response', http_success: true
- end
- context 'when disabled' do
- let(:show_domain_blocks) { 'disabled' }
- it_behaves_like 'non-cacheable error'
- end
- end
- end
- context 'with a Signature header' do
- let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
- let(:dummy_signature) { 'dummy-signature' }
- before do
- remote_actor.follow!(alice)
- end
- describe '/actor' do
- before do
- get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'cachable response', http_success: true
- end
- TestEndpoints::REQUIRE_SIGNATURE.each do |endpoint|
- describe endpoint do
- before do
- get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'non-cacheable response', http_success: true
- end
- end
- end
- context 'when enabling AUTHORIZED_FETCH mode' do
- around do |example|
- ClimateControl.modify AUTHORIZED_FETCH: 'true' do
- example.run
- end
- end
- context 'when not providing a Signature' do
- describe '/actor' do
- before do
- get '/actor', headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'cachable response', http_success: true
- end
- (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
- describe endpoint do
- before do
- get endpoint, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'non-cacheable error'
- end
- end
- end
- context 'when providing a Signature' do
- let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
- let(:dummy_signature) { 'dummy-signature' }
- before do
- remote_actor.follow!(alice)
- end
- describe '/actor' do
- before do
- get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'cachable response', http_success: true
- end
- (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
- describe endpoint do
- before do
- get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'non-cacheable response', http_success: true
- end
- end
- end
- end
- context 'when enabling LIMITED_FEDERATION_MODE mode' do
- around do |example|
- ClimateControl.modify LIMITED_FEDERATION_MODE: 'true' do
- old_limited_federation_mode = Rails.configuration.x.limited_federation_mode
- Rails.configuration.x.limited_federation_mode = true
- example.run
- Rails.configuration.x.limited_federation_mode = old_limited_federation_mode
- end
- end
- context 'when not providing a Signature' do
- describe '/actor' do
- before do
- get '/actor', headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'cachable response', http_success: true
- end
- (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
- describe endpoint do
- before do
- get endpoint, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'non-cacheable error'
- end
- end
- end
- context 'when providing a Signature from an allowed domain' do
- let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
- let(:dummy_signature) { 'dummy-signature' }
- before do
- DomainAllow.create!(domain: remote_actor.domain)
- remote_actor.follow!(alice)
- end
- describe '/actor' do
- before do
- get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'cachable response', http_success: true
- end
- (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
- describe endpoint do
- before do
- get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'non-cacheable response', http_success: true
- end
- end
- end
- context 'when providing a Signature from a non-allowed domain' do
- let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
- let(:dummy_signature) { 'dummy-signature' }
- describe '/actor' do
- before do
- get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'cachable response', http_success: true
- end
- (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
- describe endpoint do
- before do
- get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
- end
- it_behaves_like 'non-cacheable error'
- end
- end
- end
- end
- context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do
- around do |example|
- ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do
- example.run
- end
- end
- context 'when anonymously accessed' do
- TestEndpoints::ALWAYS_CACHED.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'cachable response'
- it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
- end
- end
- TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'non-cacheable response'
- end
- end
- (TestEndpoints::REQUIRE_TOKEN + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint|
- describe endpoint do
- before { get endpoint }
- it_behaves_like 'non-cacheable error'
- end
- end
- end
- context 'with an auth token' do
- TestEndpoints::ALWAYS_CACHED.each do |endpoint|
- describe endpoint do
- before do
- get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
- end
- it_behaves_like 'cachable response'
- it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
- end
- end
- TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
- describe endpoint do
- before do
- get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
- end
- it_behaves_like 'non-cacheable response'
- it 'has a Vary on Authorization' do
- expect(response_vary_headers).to include('authorization')
- end
- end
- end
- (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint|
- describe endpoint do
- before do
- get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
- end
- it_behaves_like 'non-cacheable response', http_success: true
- end
- end
- end
- end
- private
- def response_vary_headers
- response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }
- end
- end
|