Add have_cacheable_headers matcher for responses (#31727)

This commit is contained in:
Matt Jankowski 2024-09-03 11:35:19 -04:00 committed by GitHub
parent 490bdb7944
commit e1fa456c7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 96 additions and 65 deletions

View file

@ -25,10 +25,10 @@ RSpec.describe ActivityPub::CollectionsController do
context 'without signature' do context 'without signature' do
let(:remote_account) { nil } let(:remote_account) { nil }
it_behaves_like 'cacheable response'
it 'returns http success and correct media type and correct items' do it 'returns http success and correct media type and correct items' do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json' expect(response.media_type).to eq 'application/activity+json'
expect(body_as_json[:orderedItems]) expect(body_as_json[:orderedItems])
@ -64,10 +64,11 @@ RSpec.describe ActivityPub::CollectionsController do
let(:remote_account) { Fabricate(:account, domain: 'example.com') } let(:remote_account) { Fabricate(:account, domain: 'example.com') }
context 'when getting a featured resource' do context 'when getting a featured resource' do
it_behaves_like 'cacheable response'
it 'returns http success and correct media type and expected items' do it 'returns http success and correct media type and expected items' do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json' expect(response.media_type).to eq 'application/activity+json'
expect(body_as_json[:orderedItems]) expect(body_as_json[:orderedItems])

View file

@ -25,10 +25,11 @@ RSpec.describe ActivityPub::OutboxesController do
context 'with page not requested' do context 'with page not requested' do
let(:page) { nil } let(:page) { nil }
it_behaves_like 'cacheable response'
it 'returns http success and correct media type and headers and items count' do it 'returns http success and correct media type and headers and items count' do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json' expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Vary']).to be_nil expect(response.headers['Vary']).to be_nil
expect(body[:totalItems]).to eq 4 expect(body[:totalItems]).to eq 4
@ -59,10 +60,11 @@ RSpec.describe ActivityPub::OutboxesController do
context 'with page requested' do context 'with page requested' do
let(:page) { 'true' } let(:page) { 'true' }
it_behaves_like 'cacheable response'
it 'returns http success and correct media type and vary header and items' do it 'returns http success and correct media type and vary header and items' do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json' expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Vary']).to include 'Signature' expect(response.headers['Vary']).to include 'Signature'

View file

@ -68,10 +68,11 @@ RSpec.describe ActivityPub::RepliesController do
let(:parent_visibility) { :public } let(:parent_visibility) { :public }
let(:page_json) { body_as_json[:first] } let(:page_json) { body_as_json[:first] }
it_behaves_like 'cacheable response'
it 'returns http success and correct media type' do it 'returns http success and correct media type' do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json' expect(response.media_type).to eq 'application/activity+json'
end end

View file

@ -72,13 +72,12 @@ describe StatusesController do
context 'with JSON' do context 'with JSON' do
let(:format) { 'json' } let(:format) { 'json' }
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'renders ActivityPub Note object successfully', :aggregate_failures do it 'renders ActivityPub Note object successfully', :aggregate_failures do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.headers).to include( expect(response.headers).to include(
'Vary' => 'Accept, Accept-Language, Cookie',
'Content-Type' => include('application/activity+json'), 'Content-Type' => include('application/activity+json'),
'Link' => satisfy { |header| header.to_s.include?('activity+json') } 'Link' => satisfy { |header| header.to_s.include?('activity+json') }
) )
@ -380,13 +379,11 @@ describe StatusesController do
context 'with JSON' do context 'with JSON' do
let(:format) { 'json' } let(:format) { 'json' }
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'renders ActivityPub Note object successfully', :aggregate_failures do it 'renders ActivityPub Note object successfully', :aggregate_failures do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.headers).to include( expect(response.headers).to include(
'Vary' => 'Accept, Accept-Language, Cookie',
'Content-Type' => include('application/activity+json'), 'Content-Type' => include('application/activity+json'),
'Link' => satisfy { |header| header.to_s.include?('activity+json') } 'Link' => satisfy { |header| header.to_s.include?('activity+json') }
) )

View file

@ -130,6 +130,7 @@ describe 'Accounts show response' do
it 'returns a JSON version of the account', :aggregate_failures do it 'returns a JSON version of the account', :aggregate_failures do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
.and have_attributes( .and have_attributes(
media_type: eq('application/activity+json') media_type: eq('application/activity+json')
) )
@ -137,8 +138,6 @@ describe 'Accounts show response' do
expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
context 'with authorized fetch mode' do context 'with authorized fetch mode' do
let(:authorized_fetch_mode) { true } let(:authorized_fetch_mode) { true }
@ -179,6 +178,7 @@ describe 'Accounts show response' do
it 'returns a JSON version of the account', :aggregate_failures do it 'returns a JSON version of the account', :aggregate_failures do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
.and have_attributes( .and have_attributes(
media_type: eq('application/activity+json') media_type: eq('application/activity+json')
) )
@ -186,8 +186,6 @@ describe 'Accounts show response' do
expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
context 'with authorized fetch mode' do context 'with authorized fetch mode' do
let(:authorized_fetch_mode) { true } let(:authorized_fetch_mode) { true }
@ -215,10 +213,10 @@ describe 'Accounts show response' do
get short_account_path(username: account.username, format: format) get short_account_path(username: account.username, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses', :aggregate_failures do it 'responds with correct statuses', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.body).to include(status_tag_for(status_media)) expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to include(status_tag_for(status_self_reply)) expect(response.body).to include(status_tag_for(status_self_reply))
expect(response.body).to include(status_tag_for(status)) expect(response.body).to include(status_tag_for(status))
@ -234,10 +232,11 @@ describe 'Accounts show response' do
get short_account_with_replies_path(username: account.username, format: format) get short_account_with_replies_path(username: account.username, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with replies', :aggregate_failures do it 'responds with correct statuses with replies', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.body).to include(status_tag_for(status_media)) expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to include(status_tag_for(status_reply)) expect(response.body).to include(status_tag_for(status_reply))
expect(response.body).to include(status_tag_for(status_self_reply)) expect(response.body).to include(status_tag_for(status_self_reply))
@ -253,10 +252,10 @@ describe 'Accounts show response' do
get short_account_media_path(username: account.username, format: format) get short_account_media_path(username: account.username, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with media', :aggregate_failures do it 'responds with correct statuses with media', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.body).to include(status_tag_for(status_media)) expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to_not include(status_tag_for(status_direct)) expect(response.body).to_not include(status_tag_for(status_direct))
expect(response.body).to_not include(status_tag_for(status_private)) expect(response.body).to_not include(status_tag_for(status_private))
@ -277,10 +276,11 @@ describe 'Accounts show response' do
get short_account_tag_path(username: account.username, tag: tag, format: format) get short_account_tag_path(username: account.username, tag: tag, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with a tag', :aggregate_failures do it 'responds with correct statuses with a tag', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.body).to include(status_tag_for(status_tag)) expect(response.body).to include(status_tag_for(status_tag))
expect(response.body).to_not include(status_tag_for(status_direct)) expect(response.body).to_not include(status_tag_for(status_direct))
expect(response.body).to_not include(status_tag_for(status_media)) expect(response.body).to_not include(status_tag_for(status_media))

View file

@ -9,11 +9,10 @@ describe 'Custom stylesheets' do
it 'returns http success' do it 'returns http success' do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers
.and have_attributes( .and have_attributes(
content_type: match('text/css') content_type: match('text/css')
) )
end end
it_behaves_like 'cacheable response'
end end
end end

View file

@ -17,6 +17,7 @@ RSpec.describe 'Instance actor endpoint' do
it 'returns http success with correct media type and body' do it 'returns http success with correct media type and body' do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers
expect(response.content_type) expect(response.content_type)
.to start_with('application/activity+json') .to start_with('application/activity+json')
expect(body_as_json) expect(body_as_json)
@ -32,8 +33,6 @@ RSpec.describe 'Instance actor endpoint' do
url: about_more_url(instance_actor: true) url: about_more_url(instance_actor: true)
) )
end end
it_behaves_like 'cacheable response'
end end
context 'with limited federation mode disabled' do context 'with limited federation mode disabled' do

View file

@ -9,6 +9,7 @@ describe 'Manifest' do
it 'returns http success' do it 'returns http success' do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers
.and have_attributes( .and have_attributes(
content_type: match('application/json') content_type: match('application/json')
) )
@ -18,7 +19,5 @@ describe 'Manifest' do
name: 'Mastodon' name: 'Mastodon'
) )
end end
it_behaves_like 'cacheable response'
end end
end end

View file

@ -8,16 +8,15 @@ RSpec.describe 'Tags' do
let(:tag) { Fabricate :tag } let(:tag) { Fabricate :tag }
context 'with HTML format' do context 'with HTML format' do
# TODO: Convert the cacheable response shared example into a matcher, # TODO: Update the have_cacheable_headers matcher to operate on capybara sessions
# remove this example, rely on system spec (which should use matcher) # Remove this example, rely on system spec (which should use matcher)
before { get tag_path(tag) } before { get tag_path(tag) }
it 'returns http success' do it 'returns http success' do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
end end
context 'with JSON format' do context 'with JSON format' do
@ -26,11 +25,10 @@ RSpec.describe 'Tags' do
it 'returns http success' do it 'returns http success' do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.content_type) expect(response.content_type)
.to start_with('application/activity+json') .to start_with('application/activity+json')
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
end end
context 'with RSS format' do context 'with RSS format' do
@ -39,11 +37,10 @@ RSpec.describe 'Tags' do
it 'returns http success' do it 'returns http success' do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.content_type) expect(response.content_type)
.to start_with('application/rss+xml') .to start_with('application/rss+xml')
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
end end
end end

View file

@ -1,14 +0,0 @@
# frozen_string_literal: true
shared_examples 'cacheable response' do |expects_vary: false|
it 'sets correct cache and vary headers and does not set cookies or session', :aggregate_failures do
expect(response.cookies).to be_empty
expect(response.headers['Set-Cookies']).to be_nil
expect(session).to be_empty
expect(response.headers['Vary']).to include(expects_vary) if expects_vary
expect(response.headers['Cache-Control']).to include('public')
end
end

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
RSpec::Matchers.define :have_cacheable_headers do
match do |response|
@response = response
@errors = [].tap do |errors|
errors << check_cookies
errors << check_cookie_headers
errors << check_session
errors << check_cache_control
errors << check_vary if @expected_vary.present?
end
@errors.compact.empty?
end
chain :with_vary do |string|
@expected_vary = string
end
failure_message do
<<~ERROR
Expected that the response would be cacheable but it was not:
- #{@errors.compact.join("\n - ")}
ERROR
end
def check_vary
puts @expected_vary
pp @response.headers
"Response `Vary` header does not contain `#{@expected_vary}`" unless @response.headers['Vary'].include?(@expected_vary)
end
def check_cookies
'Reponse cookies are present' unless @response.cookies.empty?
end
def check_cookie_headers
'Response `Set-Cookies` headers are present' if @response.headers['Set-Cookies'].present?
end
def check_session
'The session is not empty' unless session.empty?
end
def check_cache_control
'The `Cache-Control` header does not contain `public`' unless @response.headers['Cache-Control'].include?('public')
end
end