このコミットが含まれているのは:
2026-11-01 18:18:54 +09:00
コミット 22e57564a5
18個のファイルの変更、313行の追加、121行の削除
+24 -2
ファイルの表示
@@ -14,7 +14,7 @@ RSpec.describe 'Counts native GET acknowledgement', type: :request do
allow(CountsExportDownload).to receive(:directory).and_return(@counts_download_dir)
end
def ready_job(format)
def ready_job(format, payload: 'payload')
CountsExportJobs.pulse
post '/counts/export/jobs', params: { resource: 'posts', definition: 'history',
mode: 'details', columns: 'resource_id', locale: 'ja', format: }
@@ -22,7 +22,7 @@ RSpec.describe 'Counts native GET acknowledgement', type: :request do
job = response.parsed_body.fetch('job')
signature = CountsExportJobs.verifier.verified(job, purpose: 'counts_export_job')
owner = signature.fetch('owner')
exporter = instance_double(CountsExporter, resource: 'posts', format:, body: ['payload'])
exporter = instance_double(CountsExporter, resource: 'posts', format:, body: [payload])
file = HistoryAccess.set(user: nil) { CountsExportDownload.prepare(exporter, owner) }
state = CountsExportJobs.read(signature.fetch('id'))
state.merge!('status' => 'ready', 'token' => file)
@@ -33,6 +33,28 @@ RSpec.describe 'Counts native GET acknowledgement', type: :request do
end
['csv', 'json'].each do |format|
it "delivers every byte of a multi-chunk #{ format } file through HTTP" do
sign_in_as(create(:user, :member))
rows = Array.new(5000) { |index| { id: index, name: '日本語,名称' } }
payload = if format == 'json'
rows.to_json
else
"id,name\r\n" + rows.map { CSV.generate_line(_1.values) }.join
end
job, file, attempt = ready_job(format, payload:)
owner = CountsExportJobs.verifier.verified(job, purpose: 'counts_export_job').fetch('owner')
metadata = HistoryAccess.set(user: nil) { CountsExportDownload.find(file, owner) }
expected = File.binread(metadata.fetch('path'))
expect(expected.bytesize).to be > 64 * 1024
get "/counts/export/files/#{ file }", params: { job:, attempt: }
expect(response).to have_http_status(:ok)
expect(response.body.bytesize).to eq(expected.bytesize)
expect(response.body.b).to eq(expected)
expect(response.body.b).to eq(payload.b)
get "/counts/export/jobs/#{ job }"
expect(response.parsed_body['download_status']).to eq('started')
end
it "acknowledges only an authorised #{ format } GET body, not HEAD or acceptance" do
job, file, attempt = ready_job(format)
get "/counts/export/jobs/#{ job }"
+26
ファイルの表示
@@ -50,6 +50,32 @@ RSpec.describe 'Counts table API', type: :request do
def versions(result) = result.fetch('versions').map { _1.fetch('version_no') }
it 'identifies a totals timeout without exposing SQL in the error response' do
allow_any_instance_of(CountsPivotTotals).to receive(:call)
.and_raise(CountsPivotTotals::TimedOut, 'private SQL')
get '/counts/query', params: base.merge(mode: 'summary', rows: 'created_at:month')
expect(response).to have_http_status(:service_unavailable)
expect(response.parsed_body).to eq('error' => 'counts_query_timeout',
'stage' => 'totals', 'timeout_seconds' => 30)
expect(response.body).not_to include('private SQL')
end
it 'applies the same optimizer safeguards to the paginated detail SELECT' do
statements = []
listener = lambda do |_name, _start, _finish, _id, payload|
statements << payload[:sql]
end
ActiveSupport::Notifications.subscribed(listener, 'sql.active_record') do
result = query(tags: 'alpha', limit: 1)
expect(result['count']).to eq(2)
expect(versions(result)).to eq([1])
end
details = statements.find { _1.include?('AS counts_latest') }
expect(details).not_to be_nil
expect(details).to include('MAX_EXECUTION_TIME(30000)',
"SET_VAR(optimizer_switch='semijoin=off')")
end
it 'separates latest and history and defaults to stable detail ordering and limit 100' do
history = query
expect(versions(history)).to eq([1, 2, 3])
+23 -3
ファイルの表示
@@ -211,18 +211,37 @@ RSpec.describe 'Counts download acknowledgement' do
end
RSpec.describe CountsExportDownload::ResponseBody do
it 'survives Rails buffer enumeration without closing before the first byte' do
Dir.mktmpdir('counts-body-spec') do |directory|
path = File.join(directory, 'file')
payload = 'data' * 20_000
File.binwrite(path, payload)
states = []
source = ActionDispatch::Response::FileBody.new(path)
body = described_class.new(source) { |state| states << state; true }
response = ActionDispatch::Response.new
response.body = body
deferred = response.stream.to_ary
expect(states).to eq([])
expect(deferred.to_a.join).to eq(payload)
expect(states.count('started')).to eq(1)
end
end
it 'acknowledges only after the first Rack yield and closes its bounded file body' do
Dir.mktmpdir('counts-body-spec') do |directory|
path = File.join(directory, 'file')
File.binwrite(path, 'x' * (64 * 1024 + 3))
states = []
body = described_class.new(path) { |state| states << state; true }
source = ActionDispatch::Response::FileBody.new(path)
body = described_class.new(source) { |state| states << state; true }
chunks = []
body.each do |chunk|
expect(states).not_to include('started') if chunks.empty?
chunks << chunk
end
expect(chunks.map(&:bytesize)).to eq([64 * 1024, 3])
expect(chunks.join).to eq('x' * (64 * 1024 + 3))
expect(chunks.map(&:bytesize).max).to be <= 64 * 1024
expect(states.count('started')).to eq(1)
expect(body).not_to respond_to(:to_path)
expect { body.close }.not_to raise_error
@@ -234,7 +253,8 @@ RSpec.describe CountsExportDownload::ResponseBody do
path = File.join(directory, 'file')
File.binwrite(path, 'data')
states = []
body = described_class.new(path) { |state| states << state; true }
source = ActionDispatch::Response::FileBody.new(path)
body = described_class.new(source) { |state| states << state; true }
expect { body.each { raise IOError, 'connection closed' } }.to raise_error(IOError)
expect(states).to include('failed')
expect(states).not_to include('started')
+14
ファイルの表示
@@ -44,6 +44,20 @@ RSpec.describe 'Counts masked pivot contracts' do
result.fetch(:pivot_totals).find { _1[:row].empty? && _1[:column].empty? }.fetch(:count)
end
it 'preserves monthly name subtotals and grand totals across pages' do
settings = { 'resource' => 'tags', 'rows' => 'created_at:month name_ja' }
complete = result(admin, settings)
paged = result(admin, settings.merge('limit' => '1'))
expect(complete[:group_count]).to eq(3)
expect(grand(complete)).to eq(3)
expect(grand(paged)).to eq(3)
subtotal = paged[:pivot_totals].find do |cell|
cell[:row] == ['2023-01'] && cell[:column].empty?
end
expect(subtotal[:count]).to eq(3)
expect(paged[:cells].sum { _1[:count] }).to eq(1)
end
it 'preserves five tag occurrences after merging private tags and across pages' do
[nil, guest, member, admin].each do |user|
page = result(user)