require 'rails_helper' RSpec.describe 'List API limits', type: :request do shared_examples 'a capped list' do |default_count, minimum_count, paginated| def response_rows body = json body.is_a?(Array) ? body : body.fetch(collection_key) end it 'caps oversized limits at 100 and preserves the 100 boundary' do get path, params: { locale: 'ja', limit: 100 } expect(response).to have_http_status(:ok) expect(response_rows.size).to eq(100) boundary_rows = response_rows [101, 10_000].each do |limit| get path, params: { locale: 'ja', limit: } expect(response).to have_http_status(:ok) expect(response_rows).to eq(boundary_rows) end end it 'preserves the default, nonpositive and small positive limits' do get path, params: { locale: 'ja' } expect(response).to have_http_status(:ok) expect(response_rows.size).to eq(default_count) [0, -1].each do |limit| get path, params: { locale: 'ja', limit: } expect(response).to have_http_status(:ok) expect(response_rows.size).to eq(minimum_count) end get path, params: { locale: 'ja', limit: 2 } expect(response).to have_http_status(:ok) expect(response_rows.size).to eq(2) end if paginated it 'uses the capped limit for page offsets and preserves total count' do get path, params: { locale: 'ja', limit: 100 } first_page = response_rows expect(response).to have_http_status(:ok) expect(json.fetch('count')).to eq(101) get path, params: { locale: 'ja', limit: 100, page: 2 } expect(response).to have_http_status(:ok) expect(response_rows.size).to eq(1) second_page = response_rows expect(first_page & second_page).to be_empty get path, params: { locale: 'ja', limit: 101, page: 2 } expect(response).to have_http_status(:ok) expect(response_rows).to eq(second_page) expect(json.fetch('count')).to eq(101) end end end context 'posts' do let(:path) { '/posts' } let(:collection_key) { 'posts' } before { create_list(:post, 101) } include_examples 'a capped list', 20, 1, true end context 'tags' do let(:path) { '/tags' } let(:collection_key) { 'tags' } before { create_list(:tag, 101) } include_examples 'a capped list', 20, 1, true end context 'nico tags' do let(:path) { '/tags/nico' } let(:collection_key) { 'tags' } before { create_list(:external_tag, 101) } include_examples 'a capped list', 20, 1, true end context 'materials' do let(:path) { '/materials' } let(:collection_key) { 'materials' } before do 101.times { |index| Material.create!(url: "https://example.com/material-#{ index }") } end include_examples 'a capped list', 20, 1, true end context 'post versions' do let(:path) { '/posts/versions' } let(:collection_key) { 'versions' } let(:post) { create(:post) } before do 101.times do |index| PostVersion.create!(post:, version_no: index + 1, event_type: 'update', url: post.url, tags: '', tags_json: [], parent_post_ids: '') end end include_examples 'a capped list', 20, 1, true end context 'tag versions' do let(:path) { '/tags/versions' } let(:collection_key) { 'versions' } let(:tag) { create(:tag) } before do 101.times do |index| TagVersion.create!(tag:, version_no: index + 1, event_type: 'update', name: 'limit tag', category: 'general', aliases: '', parent_tag_ids: '', names: { ja: { name: 'limit tag', script_code: 'Jpan', auto_generated: false, aliases: [] } }) end end include_examples 'a capped list', 20, 1, true end context 'material versions' do let(:path) { '/materials/versions' } let(:collection_key) { 'versions' } let(:material) { Material.create!(url: 'https://example.com/material') } before do 101.times do |index| MaterialVersion.create!(material:, version_no: index + 1, event_type: 'update', url: material.url) end end include_examples 'a capped list', 20, 1, true end context 'theatre lists' do let(:theatre) { create(:theatre) } let(:post) { create(:post) } context 'comments' do let(:path) { "/theatres/#{ theatre.id }/comments" } before { create_list(:theatre_comment, 101, theatre:, user: theatre.created_by_user) } include_examples 'a capped list', 20, 20, false end context 'programmes' do let(:path) { "/theatres/#{ theatre.id }/programmes" } before do 101.times do |index| TheatreProgramme.create!(theatre:, post:, position: index + 1) end end include_examples 'a capped list', 100, 100, false end context 'skip events' do let(:path) { "/theatres/#{ theatre.id }/skip_events" } before do 101.times do |index| TheatreSkipEvent.create!(theatre:, post:, skipped_by_user: theatre.created_by_user, created_at: Time.utc(2026, 1, 1) + index.seconds) end end include_examples 'a capped list', 50, 50, false end end end