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) 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 "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