diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/action_result.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/action_result.rb index 053d0037c..a4dae3d74 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/action_result.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/action_result.rb @@ -27,9 +27,10 @@ def self.parse(result) } when 'File' keys = { + type: 'File', name: result[:name], mime_type: result[:mime_type], - stream: result[:content] + stream: result[:stream] } when 'Redirect' keys = { diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/action_result_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/action_result_spec.rb new file mode 100644 index 000000000..7f7339199 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/action_result_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +module ForestAdminAgent + module Utils + describe ActionResult do + describe '.parse' do + context 'when the result type is Success' do + let(:result) do + { + type: 'Success', + message: 'Done', + invalidated: %w[books authors], + html: '

ok

', + response_headers: { 'x-foo' => 'bar' } + } + end + + it 'maps Success keys and forwards response_headers' do + expect(described_class.parse(result)).to eq( + success: 'Done', + refresh: { relationships: %w[books authors] }, + html: '

ok

', + headers: { 'x-foo' => 'bar' } + ) + end + end + + context 'when the result type is Error' do + let(:result) { { type: 'Error', message: 'Boom', html: nil } } + + it 'maps Error keys with status 400' do + expect(described_class.parse(result)).to eq( + status: 400, + error: 'Boom', + html: nil, + headers: nil + ) + end + end + + context 'when the result type is Webhook' do + let(:result) do + { + type: 'Webhook', + body: { foo: 'bar' }, + headers: { 'x-h' => '1' }, + method: 'POST', + url: 'https://example.test/hook' + } + end + + it 'maps Webhook keys' do + expect(described_class.parse(result)).to eq( + webhook: { + body: { foo: 'bar' }, + headers: { 'x-h' => '1' }, + method: 'POST', + url: 'https://example.test/hook' + }, + headers: nil + ) + end + end + + context 'when the result type is File' do + let(:content) { 'binary-payload' } + let(:result) do + { + type: 'File', + name: 'report.pdf', + mime_type: 'application/pdf', + stream: content, + response_headers: { 'set-cookie' => 'token=xyz' } + } + end + + it 'maps File keys including :type so the controller can detect a file response' do + parsed = described_class.parse(result) + + expect(parsed[:type]).to eq('File') + expect(parsed[:name]).to eq('report.pdf') + expect(parsed[:mime_type]).to eq('application/pdf') + expect(parsed[:stream]).to eq(content) + expect(parsed[:headers]).to eq({ 'set-cookie' => 'token=xyz' }) + end + + it 'reads the payload from :stream (the key ResultBuilder.file writes)' do + parsed = described_class.parse(result) + expect(parsed[:stream]).to eq(content) + end + end + + context 'when the result type is Redirect' do + let(:result) { { type: 'Redirect', path: '/anywhere' } } + + it 'maps Redirect keys' do + expect(described_class.parse(result)).to eq( + redirect_to: '/anywhere', + headers: nil + ) + end + end + + context 'when the result type is unknown' do + let(:result) { { type: 'Mystery' } } + + it 'returns only the merged response_headers' do + expect(described_class.parse(result)).to eq(headers: nil) + end + end + end + end + end +end diff --git a/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb b/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb index 26682d75d..765e76aba 100644 --- a/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb +++ b/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb @@ -26,6 +26,7 @@ def forest_response(data = {}) return handle_streaming_response(data) if data.dig(:content, :type) == 'Stream' if data.dig(:content, :type) == 'File' + response.headers['Access-Control-Expose-Headers'] = 'Content-Disposition' return send_data data[:content][:stream], filename: data[:content][:name], type: data[:content][:mime_type], disposition: 'attachment' end diff --git a/packages/forest_admin_rails/spec/controllers/forest_admin_rails/forest_controller_spec.rb b/packages/forest_admin_rails/spec/controllers/forest_admin_rails/forest_controller_spec.rb index fdb2acb28..0be7ac4ec 100644 --- a/packages/forest_admin_rails/spec/controllers/forest_admin_rails/forest_controller_spec.rb +++ b/packages/forest_admin_rails/spec/controllers/forest_admin_rails/forest_controller_spec.rb @@ -281,5 +281,41 @@ module ForestAdminRails end end end + + describe '#forest_response' do + context 'when the content is a File action result' do + let(:file_data) do + { + content: { + type: 'File', + name: 'report.pdf', + mime_type: 'application/pdf', + stream: 'binary-payload' + } + } + end + + before do + allow(controller).to receive(:send_data) + end + + it 'sends the file via send_data with the filename and mime type' do + controller.send(:forest_response, file_data) + + expect(controller).to have_received(:send_data).with( + 'binary-payload', + filename: 'report.pdf', + type: 'application/pdf', + disposition: 'attachment' + ) + end + + it 'exposes Content-Disposition via CORS so cross-origin clients can read it' do + controller.send(:forest_response, file_data) + + expect(response_mock.headers['Access-Control-Expose-Headers']).to eq('Content-Disposition') + end + end + end end end