LoginSignup
5
3

More than 3 years have passed since last update.

Rspecのファイルアップロードテスト時にcommitteeでCommittee::InvalidRequestが発生する事象への対処方法

Posted at

発生しているエラー

     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>

何がしたいのか

multipart/form-data 形式でファイルアップロードを受け取るようなAPIを定義している。
APIはOpenAPI3.0の仕様に基づいて設計していて、例えば以下のようになる。

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

Swaggerのドキュメントにもある。

このAPIに対して committee gem を使ってRspecを書きたい。

テストの内容

some_controller_spec.rb
# formのパラメータ
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'
    )
  }
}

Requestとして使うパラメータは上記のように定義していて、アップロードするファイルはテスト用に用意したDocxファイル。
これを以下のようにPOSTしてAPIのフォーマットを assert_schema_conform メソッドで検証している。

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

エラーの原因

APIのRequestの仕様が type: string なのに、テストでは ActionDispatch のオブジェクトが設定されているので、期待しているのと違う、っていうエラーが発生している。
しかし、ファイルアップロードのテストなのでStringでは内部の処理がうごかないので、変えることもできない。これは困った。

という状況

エラーの発生箇所

エラーは以下の OpenAPIParser の処理で発生していう。
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)

      # ... 以下省略 ...

    end

単純に valueString クラスかどうかをみていて、Stringじゃない場合はErrorになっている。今回はここが ActionDispatch のオブジェクトなので value.kind_of?(String) が偽となりエラーが発生してしまう。

type: stringであってもformat: binaryの場合はStringクラス以外も許す

ファイルのように、format: binayの場合はエラーをRaiseしないように条件を変えてやれば良い。そのためのパッチを作成する。
バッチとして該当のメソッド coerce_and_validate(value, schema, **_keyword_args) を定義した任意のModuleを作成する。

module StringValidatorPatch
  def coerce_and_validate(value, schema, **keyword_args)
    # この処理を変更
    # 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
    # --- ここまで

    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

このように、変更したいメソッドだけを再定義したModuleを定義する。このModuleをパッチを当てたいクラス、今回はOpenAPIParser::SchemaValidator::StringValidatorクラスを再オープンし、Module#prependを使ってメソッドを上書きする。
Module#prependのリファレンス

class OpenAPIParser::SchemaValidator::StringValidator
  prepend StringValidatorPatch
end

これで Committee::InvalidRequest エラーが発生しなくなる

影響を最低限に抑える

パッチを当てることでCommittee::InvalidRequestエラーを回避することはできるが、グローバルなスコープでパッチを当ててしまうと全体に影響が出てしまう。
この変更はファイルアップロードを伴うテストの時だけ有効にしたい。
そのためRspecの必要なコンテキストでだけ反映されるように context 'some context' do ... end の中で定義することを考える。

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

    # 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
        # (以下略)
      end
    end

    class OpenAPIParser::SchemaValidator::StringValidator
      prepend StringValidatorPatch
    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

しかし、ブロックのスコープは定数を分離したり名前空間を設定したりしないので、ブロック内でクラス定義などの処理は避けたい
Rubocopの Lint/ConstantDefinitionInBlockにもひっかかってしまう。
Rspecで同様の定数定義を行う場合は、stub_const() を使って定義する。

stub_const() を使って必要な箇所にパッチを適用する

クラスの再オープンをclassキーワードを使わずに行うには Class.newを使う。
ブロックを渡すことで、クラス定義も行える

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

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

これを使って、パッチを当てたクラスを定義する。パッチ用のStringValidatorPatchmoduleは spec/supportディレクトリに string_validator_patch.rb として保存し、読み込まれるようにしておく。

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

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

Class.new() の引数にクラスを渡すと親クラスとして扱われるので、上記の場合は patchedOpenAPIParser::SchemaValidator::StringValidator の子クラスとなる。
それを stub_const() を使って定数定義をしてやれば、パッチを当てたクラスを利用することができる。

この処理を必要に応じて beforelet で実装する。

まとめ

  • Committee::InvalidRequest エラーを解消するには、Validatorクラスにパッチを当てる
  • パッチを当てるには当該メソッドだけを実装したModuleを定義し、prependを使って反映する
  • RSpecではClass.new()stub_const() を使って局所的にパッチをあてる
spec/support/string_validator_patch.rb
module StringValidatorPatch
  def coerce_and_validate(value, schema, **keyword_args)
    # この処理を変更
    # 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
    # --- ここまで

    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
      # 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
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3