[RUBY] How to deal with the event that Committee :: InvalidRequest occurs in committee during Rspec file upload test

The error that is occurring

     Committee::InvalidRequest:
       #/paths/~1contracts/post/requestBody/content/multipart~1form-data/schema/properties/original_file expected string, but received ActionDispatch::Http::UploadedFile: #<ActionDispatch::Http::UploadedFile:0x00007fa77e1f36a0>

What i want to do

It defines an API that receives file uploads in the multipart / form-data format. The API is designed based on the OpenAPI 3.0 specifications, for example:

requestBody:
  original_file:
    image/png:
      schema:
        type: string
        format: binary

You can also find it in the Swagger documentation.

I want to write Rspec for this API using committee gem.

Test content

some_controller_spec.rb


#form parameters
let(:file_upload_form) {
  {
    article_id: 1_000,
    name: 'This is good article',
    # set real existing file
    original_file: fixture_file_upload(
      Rails.root.join('sample_files/sample.docx'),
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
    )
  }
}

The parameters used as Request are defined as above, and the file to be uploaded is the Docx file prepared for testing. This is POSTed as follows and the API format is verified by the ʻassert_schema_conform` method.

some_controller_spec.rb


it 'uploads file' do
  post api_v1_images_path,
         headers: authenticated_header(user),
         params: file_upload_form

  expect(response).to have_http_status(:success)
  assert_schema_conform
end

Cause of error

The API Request specification is type: string, but in the test, the object of ʻActionDispatch` is set, so I am getting an error that it is not what I expected. However, since it is a file upload test, the internal processing does not work with String, so it cannot be changed. This was a problem.

Situation

Where the error occurred

The error occurs in the following ʻOpenAPIParser` process. [lib/openapi_parser/schema_validators/string_validator.rb#L11] (https://github.com/ota42y/openapi_parser/blob/44c640cc103bbbb9e8029e41a8889e8fd9350902/lib/openapi_parser/schema_validators/string_validator.rb#L11)

lib/openapi_parser/schema_validators/string_validator.rb



    def coerce_and_validate(value, schema, **_keyword_args)
      return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(String)

      # ...Omitted below...

    end

It simply looks at whether value is a String class, and if it is not String, it is an Error. This time, since this is an object of ʻActionDispatch, value.kind_of? (String)` becomes false and an error occurs.

Even if it is type: string, if it is format: binary, it allows other than the String class.

Like a file, in the case of format: binay, you can change the conditions so as not to raise the error. Create a patch for that. Create an arbitrary Module that defines the corresponding method coerce_and_validate (value, schema, ** _keyword_args) as a batch.

module StringValidatorPatch
  def coerce_and_validate(value, schema, **keyword_args)
    #Change this process
    # https://github.com/ota42y/openapi_parser/blob/61874f0190a86c09bdfb78de5f51cfb6ae16068b/lib/openapi_parser/schema_validators/string_validator.rb#L11
    if !value.is_a?(String) && schema.format != 'binary'
      return OpenAPIParser::ValidateError.build_error_result(value, schema)
    end
    # ---So far

    value, err = check_enum_include(value, schema)
    return [nil, err] if err

    value, err = pattern_validate(value, schema)
    return [nil, err] if err

    unless @datetime_coerce_class.nil?
      value, err = coerce_date_time(value, schema)
      return [nil, err] if err
    end

    value, err = validate_max_min_length(value, schema)
    return [nil, err] if err

    value, err = validate_email_format(value, schema)
    return [nil, err] if err

    value, err = validate_uuid_format(value, schema)
    return [nil, err] if err

    [value, nil]
  end
end

In this way, define a Module that redefines only the method you want to change. Reopen the class you want to patch this Module, this time the ʻOpenAPIParser :: SchemaValidator :: StringValidator class, and use Module # prepend` to overwrite the method. Module # prepend reference

class OpenAPIParser::SchemaValidator::StringValidator
  prepend StringValidatorPatch
end

This will eliminate the Committee :: InvalidRequest error

Minimize impact

You can avoid the Committee :: InvalidRequest error by applying a patch, but applying a patch in a global scope will affect the whole thing. I want this change to take effect only for tests involving file uploads. Therefore, consider defining it in context'some context' do ... end so that it is reflected only in the required context of Rspec.

some_controller_spec.rb


RSpec.describe SomeController, type: :request do
  context 'some context' do

    #Apply the patch in context
    module StringValidatorPatch
      def coerce_and_validate(value, schema, **keyword_args)
        if !value.is_a?(String) && schema.format != 'binary'
          return OpenAPIParser::ValidateError.build_error_result(value, schema)
        end
        # (The following is omitted)
      end
    end

    class OpenAPIParser::SchemaValidator::StringValidator
      prepend StringValidatorPatch
    end
    #Patch so far

    it 'uploads file' do
      post api_v1_images_path,
             headers: authenticated_header(user),
             params: file_upload_form

      expect(response).to have_http_status(:success)
      assert_schema_conform
    end
  end
end

However, since the scope of the block does not separate constants or set namespaces, we want to avoid processing such as class definition inside the block. It also gets stuck in Rubocop's Lint / ConstantDefinitionInBlock. If you want to define a similar constant in Rspec, use stub_const () to define it.

Use stub_const () to patch where you need it

Use Class.new to reopen a class without the class keyword .. You can also define a class by passing a block

Foo = Class.new {|c|
  def hello; 'hello'; end
}

puts Foo.new.hello # => 'hello'

Use this to define a patched class. Save the StringValidatorPatchmodule for the patch in the spec / support directory as string_validator_patch.rb so that it can be loaded.

patched = Class.new(OpenAPIParser::SchemaValidator::StringValidator) do |klass|
  klass.prepend StringValidatorPatch
end

stub_const('OpenAPIParser::SchemaValidator::StringValidator', patched)

If you pass a class to the argument of Class.new (), it will be treated as a parent class, so in the above case patched will be a child class of ʻOpenAPIParser :: SchemaValidator :: StringValidator. If you define a constant using stub_const ()`, you can use the patched class.

Implement this process with before or let as needed.

Summary

--Patch the Validator class to clear the Committee :: InvalidRequest error --To apply a patch, define a Module that implements only the method, and use prepend to reflect it. --RSpec uses Class.new () and stub_const () to patch locally

spec/support/string_validator_patch.rb


module StringValidatorPatch
  def coerce_and_validate(value, schema, **keyword_args)
    #Change this process
    # https://github.com/ota42y/openapi_parser/blob/61874f0190a86c09bdfb78de5f51cfb6ae16068b/lib/openapi_parser/schema_validators/string_validator.rb#L11
    if !value.is_a?(String) && schema.format != 'binary'
      return OpenAPIParser::ValidateError.build_error_result(value, schema)
    end
    # ---So far

    value, err = check_enum_include(value, schema)
    return [nil, err] if err

    value, err = pattern_validate(value, schema)
    return [nil, err] if err

    unless @datetime_coerce_class.nil?
      value, err = coerce_date_time(value, schema)
      return [nil, err] if err
    end

    value, err = validate_max_min_length(value, schema)
    return [nil, err] if err

    value, err = validate_email_format(value, schema)
    return [nil, err] if err

    value, err = validate_uuid_format(value, schema)
    return [nil, err] if err

    [value, nil]
  end
end

some_controller_spec.rb


RSpec.describe SomeController, type: :request do
  context 'some context' do
    let(:file_upload_form) {
      {
        article_id: 1_000,
        name: 'This is good article',
        original_file: fixture_file_upload(
          Rails.root.join('sample_files/sample.docx'),
          'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        )
      }
    }

    before do
      #Apply the patch in context
      patched = Class.new(OpenAPIParser::SchemaValidator::StringValidator) do |klass|
        klass.prepend StringValidatorPatch
      end
      stub_const('OpenAPIParser::SchemaValidator::StringValidator', patched)
    end

    it 'uploads file' do
      post api_v1_images_path,
           headers: authenticated_header(user),
           params: file_upload_form

      expect(response).to have_http_status(:success)
      assert_schema_conform
    end
  end
end

Recommended Posts

How to deal with the event that Committee :: InvalidRequest occurs in committee during Rspec file upload test
How to test file upload screen in Spring + Selenium
How to achieve file upload with Feign
How to test interrupts during Thread.sleep with JUnit
How to use "sign_in" in integration test (RSpec)
How to resolve errors that occur in the "Ruby on Rails" integration test
`bind': Address already in use --bind (2) for 127.0.0.1:3000 (Errno :: EADDRINUSE) How to deal with the error
How to save a file with the specified extension under the directory specified in Java to the list
How to debug the generated jar file in Eclipse
How to deal with the error yaml.scanner.ScannerError: while scanning for the next token that appeared in Rails environment construction with Docker
How to deal with SQLite3 :: BusyException that occurs when uploading a large number of images using ActiveStorage in seeds.rb etc.
How to correctly check the local HTML file in the browser
How to test a private method with RSpec for yourself
How to erase test image after running Rspec test with CarrierWave
How to change the file name with Xcode (Refactor Rename)
How to deal with the type that I thought about writing a Java program for 2 years
[Rails / RSpec] How to deal with element has zero size error
How to deal with 405 Method Not Allowed error in Tomcat + JSP
How to make a jar file with no dependencies in Maven
How to realize huge file upload with TERASOLUNA 5.x (= Spring MVC)
How to get the length of an audio file in java
How to realize huge file upload with Rest Template of Spring
[RSpec] How to write test code
How to interact with a server that does not crash the app
How to test a private method in Java and partially mock that method
[Rails] How to get the user information currently logged in with devise
How to display the text entered in text_area in Rails with line breaks
[Rails] How to apply the CSS used in the main app with Administrate
How to start a Docker container with a volume mounted in a batch file
[Java] How to use the File class
How to delete the wrong migration file
How to filter JUnit Test in Gradle
How to delete the migration file NO FILE
[Note] How to get started with Rspec
How to add jar file in ScalaIDE
How to achieve file download with Feign
How to test private scope with JUnit
How to deal with Precompiling assets failed.
How to write an RSpec controller test
[Rails] How to read the XML file uploaded from the screen with Hash type
How to get the ID of a user authenticated with Firebase in Swift
[Rails] How to register multiple records in the intermediate table with many-to-many association
[Rails] How to operate the helper method used in the main application with Administrate
How to set when "The constructor Empty () is not visible" occurs in junit
How to set environment variables in the properties file of Spring boot application
How to test a class that handles application.properties with SpringBoot (request: pointed out)