2019-07-25 05:21:37 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-02-07 17:06:08 +00:00
|
|
|
require 'spec_helper'
|
|
|
|
|
2020-10-26 21:08:22 +00:00
|
|
|
RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state do
|
2017-02-07 17:06:08 +00:00
|
|
|
let(:app) { double(:app) }
|
|
|
|
let(:middleware) { described_class.new(app) }
|
|
|
|
let(:app_status_code) { 200 }
|
|
|
|
let(:if_none_match) { nil }
|
2019-09-18 14:02:45 +00:00
|
|
|
let(:enabled_path) { '/gitlab-org/gitlab-foss/noteable/issue/1/notes' }
|
2020-01-24 00:08:51 +00:00
|
|
|
let(:endpoint) { 'issue_notes' }
|
2017-02-07 17:06:08 +00:00
|
|
|
|
2020-11-13 18:09:11 +00:00
|
|
|
describe '.skip!' do
|
|
|
|
it 'sets the skip header on the response' do
|
|
|
|
rsp = ActionDispatch::Response.new
|
|
|
|
rsp.set_header('Anything', 'Else')
|
|
|
|
|
|
|
|
described_class.skip!(rsp)
|
|
|
|
|
|
|
|
expect(rsp.headers.to_h).to eq(described_class::SKIP_HEADER_KEY => '1', 'Anything' => 'Else')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-07 17:06:08 +00:00
|
|
|
context 'when ETag caching is not enabled for current route' do
|
2019-09-18 14:02:45 +00:00
|
|
|
let(:path) { '/gitlab-org/gitlab-foss/tree/master/noteable/issue/1/notes' }
|
2017-02-07 17:06:08 +00:00
|
|
|
|
|
|
|
before do
|
|
|
|
mock_app_response
|
|
|
|
end
|
|
|
|
|
2020-10-26 21:08:22 +00:00
|
|
|
it 'does not add ETag headers' do
|
2017-06-12 12:29:54 +00:00
|
|
|
_, headers, _ = middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
|
|
|
|
expect(headers['ETag']).to be_nil
|
2020-10-26 21:08:22 +00:00
|
|
|
expect(headers['X-Gitlab-From-Cache']).to be_nil
|
|
|
|
expect(headers[::Gitlab::Metrics::RequestsRackMiddleware::FEATURE_CATEGORY_HEADER]).to be_nil
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'passes status code from app' do
|
2017-06-12 12:29:54 +00:00
|
|
|
status, _, _ = middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
|
|
|
|
expect(status).to eq app_status_code
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when there is no ETag in store for given resource' do
|
|
|
|
let(:path) { enabled_path }
|
|
|
|
|
|
|
|
before do
|
|
|
|
mock_app_response
|
|
|
|
mock_value_in_store(nil)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'generates ETag' do
|
2019-12-07 00:07:51 +00:00
|
|
|
expect_next_instance_of(Gitlab::EtagCaching::Store) do |instance|
|
|
|
|
expect(instance).to receive(:touch).and_return('123')
|
|
|
|
end
|
2017-02-07 17:06:08 +00:00
|
|
|
|
2017-06-12 12:29:54 +00:00
|
|
|
middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
context 'when If-None-Match header was specified' do
|
|
|
|
let(:if_none_match) { 'W/"abc"' }
|
|
|
|
|
|
|
|
it 'tracks "etag_caching_key_not_found" event' do
|
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
2020-01-24 00:08:51 +00:00
|
|
|
.with(:etag_caching_middleware_used, endpoint: endpoint)
|
2017-02-07 17:06:08 +00:00
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
2020-01-24 00:08:51 +00:00
|
|
|
.with(:etag_caching_key_not_found, endpoint: endpoint)
|
2017-02-07 17:06:08 +00:00
|
|
|
|
2017-06-12 12:29:54 +00:00
|
|
|
middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when there is ETag in store for given resource' do
|
|
|
|
let(:path) { enabled_path }
|
|
|
|
|
|
|
|
before do
|
|
|
|
mock_app_response
|
|
|
|
mock_value_in_store('123')
|
|
|
|
end
|
|
|
|
|
2020-10-26 21:08:22 +00:00
|
|
|
it 'returns the correct headers' do
|
2017-06-12 12:29:54 +00:00
|
|
|
_, headers, _ = middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
|
|
|
|
expect(headers['ETag']).to eq 'W/"123"'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-11-13 18:09:11 +00:00
|
|
|
context 'when the matching route requests that the ETag is skipped' do
|
|
|
|
let(:path) { enabled_path }
|
|
|
|
let(:app) do
|
|
|
|
proc do |_env|
|
|
|
|
response = ActionDispatch::Response.new
|
|
|
|
|
|
|
|
described_class.skip!(response)
|
|
|
|
|
|
|
|
[200, response.headers.to_h, '']
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns the correct headers' do
|
|
|
|
expect(app).to receive(:call).and_call_original
|
|
|
|
|
|
|
|
_, headers, _ = middleware.call(build_request(path, if_none_match))
|
|
|
|
|
|
|
|
expect(headers).not_to have_key('ETag')
|
|
|
|
expect(headers).not_to have_key(described_class::SKIP_HEADER_KEY)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-01-24 00:08:51 +00:00
|
|
|
shared_examples 'sends a process_action.action_controller notification' do |status_code|
|
|
|
|
let(:expected_items) do
|
|
|
|
{
|
|
|
|
etag_route: endpoint,
|
|
|
|
params: {},
|
|
|
|
format: :html,
|
|
|
|
method: 'GET',
|
|
|
|
path: enabled_path,
|
|
|
|
status: status_code
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'sends the expected payload' do
|
|
|
|
payload = payload_for('process_action.action_controller') do
|
|
|
|
middleware.call(build_request(path, if_none_match))
|
|
|
|
end
|
|
|
|
|
|
|
|
expect(payload).to include(expected_items)
|
|
|
|
|
|
|
|
expect(payload[:headers].env['HTTP_IF_NONE_MATCH']).to eq('W/"123"')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'log subscriber processes action' do
|
|
|
|
expect_any_instance_of(ActionController::LogSubscriber).to receive(:process_action)
|
|
|
|
.with(instance_of(ActiveSupport::Notifications::Event))
|
|
|
|
.and_call_original
|
|
|
|
|
|
|
|
middleware.call(build_request(path, if_none_match))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-07 17:06:08 +00:00
|
|
|
context 'when If-None-Match header matches ETag in store' do
|
|
|
|
let(:path) { enabled_path }
|
|
|
|
let(:if_none_match) { 'W/"123"' }
|
|
|
|
|
|
|
|
before do
|
|
|
|
mock_value_in_store('123')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not call app' do
|
|
|
|
expect(app).not_to receive(:call)
|
|
|
|
|
2017-06-12 12:29:54 +00:00
|
|
|
middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns status code 304' do
|
2017-06-12 12:29:54 +00:00
|
|
|
status, _, _ = middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
|
|
|
|
expect(status).to eq 304
|
|
|
|
end
|
|
|
|
|
2020-10-26 21:08:22 +00:00
|
|
|
it 'sets correct headers' do
|
|
|
|
_, headers, _ = middleware.call(build_request(path, if_none_match))
|
|
|
|
|
|
|
|
expect(headers).to include('X-Gitlab-From-Cache' => 'true',
|
|
|
|
::Gitlab::Metrics::RequestsRackMiddleware::FEATURE_CATEGORY_HEADER => 'issue_tracking')
|
|
|
|
end
|
|
|
|
|
2020-01-24 00:08:51 +00:00
|
|
|
it_behaves_like 'sends a process_action.action_controller notification', 304
|
|
|
|
|
2017-04-05 12:27:49 +00:00
|
|
|
it 'returns empty body' do
|
2017-06-12 12:29:54 +00:00
|
|
|
_, _, body = middleware.call(build_request(path, if_none_match))
|
2017-04-05 12:27:49 +00:00
|
|
|
|
|
|
|
expect(body).to be_empty
|
|
|
|
end
|
|
|
|
|
2017-02-07 17:06:08 +00:00
|
|
|
it 'tracks "etag_caching_cache_hit" event' do
|
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
2020-01-24 00:08:51 +00:00
|
|
|
.with(:etag_caching_middleware_used, endpoint: endpoint)
|
2017-02-07 17:06:08 +00:00
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
2020-01-24 00:08:51 +00:00
|
|
|
.with(:etag_caching_cache_hit, endpoint: endpoint)
|
2017-02-07 17:06:08 +00:00
|
|
|
|
2017-06-12 12:29:54 +00:00
|
|
|
middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
2017-04-03 13:17:04 +00:00
|
|
|
|
|
|
|
context 'when polling is disabled' do
|
|
|
|
before do
|
2017-06-21 13:48:12 +00:00
|
|
|
allow(Gitlab::PollingInterval).to receive(:polling_enabled?)
|
|
|
|
.and_return(false)
|
2017-04-03 13:17:04 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns status code 429' do
|
2017-06-12 12:29:54 +00:00
|
|
|
status, _, _ = middleware.call(build_request(path, if_none_match))
|
2017-04-03 13:17:04 +00:00
|
|
|
|
|
|
|
expect(status).to eq 429
|
|
|
|
end
|
2020-01-24 00:08:51 +00:00
|
|
|
|
|
|
|
it_behaves_like 'sends a process_action.action_controller notification', 429
|
2017-04-03 13:17:04 +00:00
|
|
|
end
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
context 'when If-None-Match header does not match ETag in store' do
|
|
|
|
let(:path) { enabled_path }
|
|
|
|
let(:if_none_match) { 'W/"abc"' }
|
|
|
|
|
|
|
|
before do
|
|
|
|
mock_value_in_store('123')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'calls app' do
|
|
|
|
expect(app).to receive(:call).and_return([app_status_code, {}, ['body']])
|
|
|
|
|
2017-06-12 12:29:54 +00:00
|
|
|
middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'tracks "etag_caching_resource_changed" event' do
|
|
|
|
mock_app_response
|
|
|
|
|
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
2020-01-24 00:08:51 +00:00
|
|
|
.with(:etag_caching_middleware_used, endpoint: endpoint)
|
2017-02-07 17:06:08 +00:00
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
2020-01-24 00:08:51 +00:00
|
|
|
.with(:etag_caching_resource_changed, endpoint: endpoint)
|
2017-02-07 17:06:08 +00:00
|
|
|
|
2017-06-12 12:29:54 +00:00
|
|
|
middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when If-None-Match header is not specified' do
|
|
|
|
let(:path) { enabled_path }
|
|
|
|
|
|
|
|
before do
|
|
|
|
mock_value_in_store('123')
|
|
|
|
mock_app_response
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'tracks "etag_caching_header_missing" event' do
|
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
2020-01-24 00:08:51 +00:00
|
|
|
.with(:etag_caching_middleware_used, endpoint: endpoint)
|
2017-02-07 17:06:08 +00:00
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
2020-01-24 00:08:51 +00:00
|
|
|
.with(:etag_caching_header_missing, endpoint: endpoint)
|
2017-02-07 17:06:08 +00:00
|
|
|
|
2017-06-12 12:29:54 +00:00
|
|
|
middleware.call(build_request(path, if_none_match))
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-06-07 09:11:26 +00:00
|
|
|
context 'when GitLab instance is using a relative URL' do
|
|
|
|
before do
|
|
|
|
mock_app_response
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'uses full path as cache key' do
|
|
|
|
env = {
|
|
|
|
'PATH_INFO' => enabled_path,
|
|
|
|
'SCRIPT_NAME' => '/relative-gitlab'
|
|
|
|
}
|
|
|
|
|
2019-12-07 00:07:51 +00:00
|
|
|
expect_next_instance_of(Gitlab::EtagCaching::Store) do |instance|
|
|
|
|
expect(instance).to receive(:get).with("/relative-gitlab#{enabled_path}").and_return(nil)
|
|
|
|
end
|
2017-06-07 09:11:26 +00:00
|
|
|
|
|
|
|
middleware.call(env)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-07 17:06:08 +00:00
|
|
|
def mock_app_response
|
|
|
|
allow(app).to receive(:call).and_return([app_status_code, {}, ['body']])
|
|
|
|
end
|
|
|
|
|
|
|
|
def mock_value_in_store(value)
|
2019-12-07 00:07:51 +00:00
|
|
|
allow_next_instance_of(Gitlab::EtagCaching::Store) do |instance|
|
|
|
|
allow(instance).to receive(:get).and_return(value)
|
|
|
|
end
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
|
2017-06-12 12:29:54 +00:00
|
|
|
def build_request(path, if_none_match)
|
2020-01-24 00:08:51 +00:00
|
|
|
{ 'PATH_INFO' => path,
|
|
|
|
'HTTP_IF_NONE_MATCH' => if_none_match,
|
|
|
|
'rack.input' => '',
|
|
|
|
'REQUEST_METHOD' => 'GET' }
|
|
|
|
end
|
|
|
|
|
|
|
|
def payload_for(event)
|
|
|
|
payload = nil
|
|
|
|
subscription = ActiveSupport::Notifications.subscribe event do |_, _, _, _, extra_payload|
|
|
|
|
payload = extra_payload
|
|
|
|
end
|
|
|
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
|
|
payload
|
2017-02-07 17:06:08 +00:00
|
|
|
end
|
|
|
|
end
|