From 352e3cd11c650fc8f7f2a3e4098a306b74aa5edb Mon Sep 17 00:00:00 2001 From: Butiri Cristian Date: Fri, 14 Jul 2023 04:18:11 +0300 Subject: [PATCH] Improved support for multipart/form-data example recording (#891) * Keep support for ruby 2.6+ and rubocop fixes --------- Co-authored-by: Mathieu Jobin <99191+mathieujobin@users.noreply.github.com> --- .rubocop_todo.yml | 1 + lib/apipie/extractor/collector.rb | 3 +- lib/apipie/extractor/recorder.rb | 28 ++++++++++++- spec/lib/apipie/extractor/recorder_spec.rb | 49 +++++++++++++++++++--- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c8e9c2d6..98cd81a6 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1242,6 +1242,7 @@ RSpec/VerifiedDoubles: - 'spec/lib/apipie/apipies_controller_spec.rb' - 'spec/lib/apipie/extractor/writer_spec.rb' - 'spec/lib/validators/array_validator_spec.rb' + - 'spec/lib/apipie/extractor/recorder_spec.rb' # Offense count: 1 RSpec/VoidExpect: diff --git a/lib/apipie/extractor/collector.rb b/lib/apipie/extractor/collector.rb index e25f862d..ed841d99 100644 --- a/lib/apipie/extractor/collector.rb +++ b/lib/apipie/extractor/collector.rb @@ -24,10 +24,10 @@ def ignore_call?(record) end def handle_record(record) - add_to_records(record) if ignore_call?(record) Extractor.logger.info("REST_API: skipping #{record_to_s(record)}") else + add_to_records(record) refine_description(record) end end @@ -114,4 +114,3 @@ def record_to_s(record) end end end - diff --git a/lib/apipie/extractor/recorder.rb b/lib/apipie/extractor/recorder.rb index 5e2b857e..500182d5 100644 --- a/lib/apipie/extractor/recorder.rb +++ b/lib/apipie/extractor/recorder.rb @@ -44,7 +44,7 @@ def analyze_functional_test(test_context) @path = request.path @params = request.request_parameters if [:POST, :PUT, :PATCH, :DELETE].include?(@verb) - @request_data = @params + @request_data = request.content_type == "multipart/form-data" ? reformat_multipart_data(@params) : @params else @query = request.query_string end @@ -66,8 +66,14 @@ def reformat_multipart_data(form) lines = ["Content-Type: multipart/form-data; boundary=#{MULTIPART_BOUNDARY}",''] boundary = "--#{MULTIPART_BOUNDARY}" form.each do |key, attrs| - if attrs.is_a?(String) + if attrs.is_a?(String) # rubocop:disable Style/CaseLikeIf lines << boundary << content_disposition(key) << "Content-Length: #{attrs.size}" << '' << attrs + elsif attrs.is_a?(Rack::Test::UploadedFile) || attrs.is_a?(ActionDispatch::Http::UploadedFile) + reformat_uploaded_file(boundary, attrs, key, lines) + elsif attrs.is_a?(Array) + reformat_array(boundary, attrs, key, lines) + elsif attrs.is_a?(TrueClass) || attrs.is_a?(FalseClass) + reformat_boolean(boundary, attrs, key, lines) else reformat_hash(boundary, attrs, lines) end @@ -88,6 +94,24 @@ def reformat_hash(boundary, attrs, lines) end end + def reformat_boolean(boundary, attrs, key, lines) + lines << boundary << content_disposition(key) + lines << '' << attrs.to_s + end + + def reformat_array(boundary, attrs, key, lines) + attrs.each do |item| + lines << boundary << content_disposition("#{key}[]") + lines << '' << item + end + end + + def reformat_uploaded_file(boundary, file, key, lines) + lines << boundary << %{#{content_disposition(key)}; filename="#{file.original_filename}"} + lines << "Content-Length: #{file.size}" << "Content-Type: #{file.content_type}" << "Content-Transfer-Encoding: binary" + lines << '' << %{... contents of "#{key}" ...} + end + def content_disposition(name) %{Content-Disposition: form-data; name="#{name}"} end diff --git a/spec/lib/apipie/extractor/recorder_spec.rb b/spec/lib/apipie/extractor/recorder_spec.rb index b3e02895..f5bfb5b8 100644 --- a/spec/lib/apipie/extractor/recorder_spec.rb +++ b/spec/lib/apipie/extractor/recorder_spec.rb @@ -4,6 +4,11 @@ describe 'Apipie::Extractor::Recorder' do let(:recorder) { Apipie::Extractor::Recorder.new } + let(:controller) do + controller = ActionController::Metal.new + controller.set_request!(request) + controller + end describe '#analyse_controller' do subject do @@ -19,12 +24,6 @@ request end - let(:controller) do - controller = ActionController::Metal.new - controller.set_request!(request) - controller - end - it { is_expected.to eq(action) } context 'when a api_action_matcher is configured' do @@ -37,4 +36,42 @@ it { is_expected.to eq(matcher_action) } end end + + describe '#analyse_functional_test' do + context 'with multipart-form data' do + subject do + recorder.analyse_controller(controller) + recorder.analyze_functional_test(test_context) + recorder.record[:request_data] + end + + let(:test_context) do + double(controller: controller, request: request, response: ActionDispatch::Response.new(200)) + end + + let(:file) do + instance_double( + ActionDispatch::Http::UploadedFile, + original_filename: 'file.txt', + content_type: 'text/plain', + size: '1MB' + ) + end + + let(:request) do + request = ActionDispatch::Request.new({}) + request.request_method = 'POST' + request.headers['Content-Type'] = 'multipart/form-data' + request.request_parameters = { file: file } + request + end + + before do + allow(file).to receive(:is_a?).and_return(false) + allow(file).to receive(:is_a?).with(ActionDispatch::Http::UploadedFile).and_return(true) + end + + it { is_expected.to include("filename=\"#{file.original_filename}\"") } + end + end end