require 'spec_helper' describe API::Helpers::Pagination do let(:resource) { Project.all } subject do Class.new.include(described_class).new end describe '#paginate (keyset pagination)' do let(:value) { spy('return value') } before do allow(value).to receive(:to_query).and_return(value) allow(subject).to receive(:header).and_return(value) allow(subject).to receive(:params).and_return(value) allow(subject).to receive(:request).and_return(value) end context 'when resource can be paginated' do let!(:projects) do [ create(:project, name: 'One'), create(:project, name: 'Two'), create(:project, name: 'Three') ].sort_by { |e| -e.id } # sort by id desc (this is the default sort order for the API) end describe 'first page' do before do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', per_page: 2 }) end it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 2 end it 'returns the first two records (by id desc)' do expect(subject.paginate(resource)).to eq(projects[0..1]) end it 'adds appropriate headers' do expect_header('X-Per-Page', '2') expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[1].id}&pagination=keyset&per_page=2") expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') end subject.paginate(resource) end end describe 'second page' do before do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[1].id }) end it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 1 end it 'returns the third record' do expect(subject.paginate(resource)).to eq(projects[2..2]) end it 'adds appropriate headers' do expect_header('X-Per-Page', '2') expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[2].id}&pagination=keyset&per_page=2") expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') end subject.paginate(resource) end end describe 'third page' do before do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[2].id }) end it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 0 end it 'adds appropriate headers' do expect_header('X-Per-Page', '2') expect(subject).not_to receive(:header).with('Link') subject.paginate(resource) end end context 'if order' do context 'is not present' do before do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', per_page: 2 }) end it 'is not present it adds default order(:id) desc' do resource.order_values = [] paginated_relation = subject.paginate(resource) expect(resource.order_values).to be_empty expect(paginated_relation.order_values).to be_present expect(paginated_relation.order_values.size).to eq(1) expect(paginated_relation.order_values.first).to be_descending expect(paginated_relation.order_values.first.expr.name).to eq :id end end context 'is present' do let(:resource) { Project.all.order(name: :desc) } let!(:projects) do [ create(:project, name: 'One'), create(:project, name: 'Two'), create(:project, name: 'Three'), create(:project, name: 'Three'), # Note the duplicate name create(:project, name: 'Four'), create(:project, name: 'Five'), create(:project, name: 'Six') ] # if we sort this by name descending, id descending, this yields: # { # 2 => "Two", # 4 => "Three", # 3 => "Three", # 7 => "Six", # 1 => "One", # 5 => "Four", # 6 => "Five" # } # # (key is the id) end it 'it also orders by primary key' do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', per_page: 2 }) paginated_relation = subject.paginate(resource) expect(paginated_relation.order_values).to be_present expect(paginated_relation.order_values.size).to eq(2) expect(paginated_relation.order_values.first).to be_descending expect(paginated_relation.order_values.first.expr.name).to eq :name expect(paginated_relation.order_values.second).to be_descending expect(paginated_relation.order_values.second.expr.name).to eq :id end it 'it returns the right records (first page)' do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', per_page: 2 }) result = subject.paginate(resource) expect(result.first).to eq(projects[1]) expect(result.second).to eq(projects[3]) end it 'it returns the right records (second page)' do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 }) result = subject.paginate(resource) expect(result.first).to eq(projects[2]) expect(result.second).to eq(projects[6]) end it 'it returns the right records (third page), note increased per_page' do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5 }) result = subject.paginate(resource) expect(result.size).to eq(3) expect(result.first).to eq(projects[0]) expect(result.second).to eq(projects[4]) expect(result.last).to eq(projects[5]) end it 'it returns the right link to the next page' do allow(subject).to receive(:params) .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 }) expect_header('X-Per-Page', '2') expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2") expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') end subject.paginate(resource) end end end end end describe '#paginate (default offset-based pagination)' do let(:value) { spy('return value') } before do allow(value).to receive(:to_query).and_return(value) allow(subject).to receive(:header).and_return(value) allow(subject).to receive(:params).and_return(value) allow(subject).to receive(:request).and_return(value) end describe 'required instance methods' do let(:return_spy) { spy } it 'requires some instance methods' do expect_message(:header) expect_message(:params) expect_message(:request) subject.paginate(resource) end end context 'when resource can be paginated' do before do create_list(:project, 3) end describe 'first page' do before do allow(subject).to receive(:params) .and_return({ page: 1, per_page: 2 }) end shared_examples 'response with pagination headers' do it 'adds appropriate headers' do expect_header('X-Total', '3') expect_header('X-Total-Pages', '2') expect_header('X-Per-Page', '2') expect_header('X-Page', '1') expect_header('X-Next-Page', '2') expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| expect(val).to include('rel="first"') expect(val).to include('rel="last"') expect(val).to include('rel="next"') expect(val).not_to include('rel="prev"') end subject.paginate(resource) end end shared_examples 'paginated response' do it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 2 end it 'executes only one SELECT COUNT query' do expect { subject.paginate(resource) }.to make_queries_matching(/SELECT COUNT/, 1) end end context 'when the api_kaminari_count_with_limit feature flag is unset' do it_behaves_like 'paginated response' it_behaves_like 'response with pagination headers' end context 'when the api_kaminari_count_with_limit feature flag is disabled' do before do stub_feature_flags(api_kaminari_count_with_limit: false) end it_behaves_like 'paginated response' it_behaves_like 'response with pagination headers' end context 'when the api_kaminari_count_with_limit feature flag is enabled' do before do stub_feature_flags(api_kaminari_count_with_limit: true) end context 'when resources count is less than MAX_COUNT_LIMIT' do before do stub_const("::Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT", 4) end it_behaves_like 'paginated response' it_behaves_like 'response with pagination headers' end context 'when resources count is more than MAX_COUNT_LIMIT' do before do stub_const("::Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT", 2) end it_behaves_like 'paginated response' it 'does not return the X-Total and X-Total-Pages headers' do expect_no_header('X-Total') expect_no_header('X-Total-Pages') expect_header('X-Per-Page', '2') expect_header('X-Page', '1') expect_header('X-Next-Page', '2') expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| expect(val).to include('rel="first"') expect(val).not_to include('rel="last"') expect(val).to include('rel="next"') expect(val).not_to include('rel="prev"') end subject.paginate(resource) end end end end describe 'second page' do before do allow(subject).to receive(:params) .and_return({ page: 2, per_page: 2 }) end it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 1 end it 'adds appropriate headers' do expect_header('X-Total', '3') expect_header('X-Total-Pages', '2') expect_header('X-Per-Page', '2') expect_header('X-Page', '2') expect_header('X-Next-Page', '') expect_header('X-Prev-Page', '1') expect_header('Link', anything) do |_key, val| expect(val).to include('rel="first"') expect(val).to include('rel="last"') expect(val).to include('rel="prev"') expect(val).not_to include('rel="next"') end subject.paginate(resource) end end context 'if order' do it 'is not present it adds default order(:id) if no order is present' do resource.order_values = [] paginated_relation = subject.paginate(resource) expect(resource.order_values).to be_empty expect(paginated_relation.order_values).to be_present expect(paginated_relation.order_values.first).to be_ascending expect(paginated_relation.order_values.first.expr.name).to eq :id end it 'is present it does not add anything' do paginated_relation = subject.paginate(resource.order(created_at: :desc)) expect(paginated_relation.order_values).to be_present expect(paginated_relation.order_values.first).to be_descending expect(paginated_relation.order_values.first.expr.name).to eq :created_at end end end context 'when resource empty' do describe 'first page' do before do allow(subject).to receive(:params) .and_return({ page: 1, per_page: 2 }) end it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 0 end it 'adds appropriate headers' do expect_header('X-Total', '0') expect_header('X-Total-Pages', '1') expect_header('X-Per-Page', '2') expect_header('X-Page', '1') expect_header('X-Next-Page', '') expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| expect(val).to include('rel="first"') expect(val).to include('rel="last"') expect(val).not_to include('rel="prev"') expect(val).not_to include('rel="next"') expect(val).not_to include('page=0') end subject.paginate(resource) end end end end def expect_header(*args, &block) expect(subject).to receive(:header).with(*args, &block) end def expect_no_header(*args, &block) expect(subject).not_to receive(:header).with(*args) end def expect_message(method) expect(subject).to receive(method) .at_least(:once).and_return(value) end end