require 'rails_helper' require 'tmpdir' RSpec.describe 'Counts native GET acknowledgement', type: :request do include ActiveSupport::Testing::TimeHelpers around do |example| Dir.mktmpdir('counts-download-request') do |path| @counts_download_dir = Pathname.new(path) example.run end end before do allow(CountsExportDownload).to receive(:directory).and_return(@counts_download_dir) end 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: } expect(response).to have_http_status(:accepted) 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]) file = HistoryAccess.set(user: nil) { CountsExportDownload.prepare(exporter, owner) } state = CountsExportJobs.read(signature.fetch('id')) state.merge!('status' => 'ready', 'token' => file) CountsExportJobs.write(state) post "/counts/export/jobs/#{ job }/download" expect(response).to have_http_status(:ok) [job, file, response.parsed_body.fetch('download_attempt')] 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 }" expect(response.parsed_body['download_status']).to eq('pending') head "/counts/export/files/#{ file }", params: { job:, attempt: } expect(response).to have_http_status(:method_not_allowed) get "/counts/export/jobs/#{ job }" expect(response.parsed_body['download_status']).to eq('pending') get "/counts/export/files/#{ file }", params: { job:, attempt: } expect(response).to have_http_status(:ok) expect(response.headers['Content-Disposition']).to include("counts-posts.#{ format }") expect(response.body).to eq('payload') get "/counts/export/jobs/#{ job }" expect(response.parsed_body['download_status']).to eq('started') get "/counts/export/files/#{ file }", params: { job:, attempt: } expect(response).to have_http_status(:not_found) end end it 'rejects wrong attempts, cancellation and a newly discarded resource' do job, file, attempt = ready_job('json') get "/counts/export/files/#{ file }", params: { job:, attempt: 'wrong' } expect(response).to have_http_status(:not_found) delete "/counts/export/jobs/#{ job }" expect(response).to have_http_status(:ok) get "/counts/export/files/#{ file }", params: { job:, attempt: } expect(response).to have_http_status(:not_found) job, file, attempt = ready_job('json') record = create(:post) PostVersion.create!(post: record, version_no: 1, event_type: 'discard', title: 'private', url: record.url, tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024)) get "/counts/export/files/#{ file }", params: { job:, attempt: } expect(response).to have_http_status(:not_found) expect(response.body).not_to include('payload', 'private') end it 'does not let a second browser session inspect the job or download its file' do job, file, attempt = ready_job('json') other = ActionDispatch::Integration::Session.new(Rails.application) other.get("/counts/export/jobs/#{ job }") expect(other.response).to have_http_status(:not_found) other.get("/counts/export/files/#{ file }", params: { job:, attempt: }) expect(other.response).to have_http_status(:not_found) expect(other.response.body).not_to include('payload') end it 'does not acknowledge an expired attempt' do job, file, attempt = ready_job('json') travel_to(Time.current + 46.seconds) do get "/counts/export/files/#{ file }", params: { job:, attempt: } expect(response).to have_http_status(:not_found) get "/counts/export/jobs/#{ job }" expect(response.parsed_body['download_status']).to eq('timeout') end end it 'records a missing file without accepting the GET as started' do job, file, attempt = ready_job('json') owner = CountsExportJobs.verifier.verified(job, purpose: 'counts_export_job').fetch('owner') metadata = HistoryAccess.set(user: nil) { CountsExportDownload.find(file, owner) } File.unlink(metadata.fetch('path')) get "/counts/export/files/#{ file }", params: { job:, attempt: } expect(response).to have_http_status(:not_found) get "/counts/export/jobs/#{ job }" expect(response.parsed_body['download_status']).to eq('unavailable') end end