Rswag: Are multiple responses allowed for the same code?

Created on 2 Jun 2018  路  4Comments  路  Source: rswag/rswag

I'm assuming this is a limitation of Swagger, however when I write a spec like this, with multiple response "422" blocks:

require "swagger_helper"

describe "User registration API" do
  let!(:user) { FactoryBot.create(:user) }
  let(:Authorization) { "Bearer #{::InternalApi::UserToken.encode(user)}" }

  path '/users/registrations' do
    patch "updates user email and/or password" do
      security [ bearer: [] ]
      consumes 'application/json'
      parameter name: :body, in: :body, required: true, schema: {
        type: :object,
        properties: {
          current_password: { type: :string },
          password: { type: :string },
          email: { type: :string },
        },
        required: [ "current_password" ]
      }

      response "200", "email and/or password updated" do
        let(:body) {{
          current_password: FactoryBot.attributes_for(:user)[:password],
          password: "newpassword",
          email: "[email protected]",
        }}

        run_test!
      end

      response "422", "incorrect current password" do
        let(:body) {{
          current_password: "wrongpw",
          password: "newpassword",
        }}

        before do |example|
          submit_request(example.metadata)
        end

        it "returns expected response" do |example|
          assert_response_matches_metadata(example.metadata)
          expect(response.body).to eq('{"error":"Current password is invalid"}')
        end
      end

      response "422", "invalid email" do
        let(:body) {{
          current_password: FactoryBot.attributes_for(:user)[:password],
          email: "bademail",
        }}

        before do |example|
          submit_request(example.metadata)
        end

        it "returns expected response" do |example|
          assert_response_matches_metadata(example.metadata)
          expect(response.body).to eq('{"error":"Email is invalid"}')
        end
      end
    end
  end
end

My 3 specs pass with Rspec just fine, however the generated Swagger UI documentation only references the final 422 definition for "invalid email":

screenshot from 2018-06-02 13-24-43

Since it seems like a Swagger restriction, I switched to using context blocks under a single response block, which works just fine:

require "swagger_helper"

describe "User registration API" do
  let!(:user) { FactoryBot.create(:user) }
  let(:Authorization) { "Bearer #{::InternalApi::UserToken.encode(user)}" }

  path '/users/registrations' do
    patch "updates user email and/or password" do
      security [ bearer: [] ]
      consumes 'application/json'
      parameter name: :body, in: :body, required: true, schema: {
        type: :object,
        properties: {
          current_password: { type: :string },
          password: { type: :string },
          email: { type: :string },
        },
        required: [ "current_password" ]
      }

      response "200", "email and/or password updated" do
        let(:body) {{
          current_password: FactoryBot.attributes_for(:user)[:password],
          password: "newpassword",
          email: "[email protected]",
        }}

        run_test!
      end

      response "422", "incorrect current password or invalid email" do
        context "incorrect current password" do
          let(:body) {{
            current_password: "wrongpw",
            password: "newpassword",
          }}

          before do |example|
            submit_request(example.metadata)
          end

          it "returns expected response" do |example|
            assert_response_matches_metadata(example.metadata)
            expect(response.body).to eq('{"error":"Current password is invalid"}')
          end
        end

        context "invalid email" do
          let(:body) {{
            current_password: FactoryBot.attributes_for(:user)[:password],
            email: "bademail",
          }}

          before do |example|
            submit_request(example.metadata)
          end

          it "returns expected response" do |example|
            assert_response_matches_metadata(example.metadata)
            expect(response.body).to eq('{"error":"Email is invalid"}')
          end
        end
      end
    end
  end
end

However maybe rswag should raise an error if multiple response codes are defined for the same method, to anticipate dumb dumbs like me? Just a thought.

Thanks for this gem btw, I like this test-integrated approach a lot better than annotating the controllers like other gems do!

Most helpful comment

You can't have multiple responses for the same response code because swagger.json uses the response code as a key by default. You can get around this by adding an after hook to the example which assigns the response to a new key, e.g:

after do |example|
  example.metadata[:response][:examples] = { 
    # make sure the key here is unique
    'wrong password' => JSON.parse(response.body, symbolize_names: true) 
  }
end

You can also hack around the unique key issue by adding whitespace to the response code and explicitly matching the response when whitespace is added, but this leads to misordered results.

response "200", "Found stuff" do
  before do |example|
    submit_request(example.metadata)
  end
  it 'returns 200' do
    expect(status).to eq(200) 
  end
end
# note the space after 200
response "200 ", "Found other stuff" do
  before do |example|
    submit_request(example.metadata)
  end
  it 'actually returns 200, not "200 "' do
    expect(status).to eq(200) 
  end
end

You can handle this lots of different ways. You could also use the oneOf key. It seems the right way to handle this has been a subject of debate in both swagger and openapi for a while though. Personally I prefer using the examples after hook.
https://github.com/OAI/OpenAPI-Specification/issues/270
https://github.com/swagger-api/swagger-ui/issues/3803

All 4 comments

You can't have multiple responses for the same response code because swagger.json uses the response code as a key by default. You can get around this by adding an after hook to the example which assigns the response to a new key, e.g:

after do |example|
  example.metadata[:response][:examples] = { 
    # make sure the key here is unique
    'wrong password' => JSON.parse(response.body, symbolize_names: true) 
  }
end

You can also hack around the unique key issue by adding whitespace to the response code and explicitly matching the response when whitespace is added, but this leads to misordered results.

response "200", "Found stuff" do
  before do |example|
    submit_request(example.metadata)
  end
  it 'returns 200' do
    expect(status).to eq(200) 
  end
end
# note the space after 200
response "200 ", "Found other stuff" do
  before do |example|
    submit_request(example.metadata)
  end
  it 'actually returns 200, not "200 "' do
    expect(status).to eq(200) 
  end
end

You can handle this lots of different ways. You could also use the oneOf key. It seems the right way to handle this has been a subject of debate in both swagger and openapi for a while though. Personally I prefer using the examples after hook.
https://github.com/OAI/OpenAPI-Specification/issues/270
https://github.com/swagger-api/swagger-ui/issues/3803

Not sure about throwing an error, I think the warning may be a better option here.

If you'd like to demonstrate different variants for one response code you can use examples for that

response 422, 'failure' do
  examples 'application/json' => [
    {
      key: 'is_missing',
      message: 'is missing',
      payload: { path: 'contact.legal_agreement' },
      type: 'params'
    }, {
      key: 'has_already_been_taken',
      message: 'has already been taken',
      payload: { path: 'contact.login' },
      type: 'params'
    }, {
      key: 'is_in_invalid_format',
      message: 'is in invalid format',
      payload: { path: 'contact.name' },
      type: 'params'
    }, {
      key: 'length_must_be_within',
      message: 'length must be within 8 - 20',
      payload: {
        path: 'contact.password',
        range: ['8', '20']
      },
      type: 'params'
    }
  ]

  context 'required field is missing' do
  end

  context 'user is already registered' do
  end

  context 'contact name is in invalid format' do
  end

  context 'contact password is too short' do
  end
end

An advantage of that is that It will show all variants in the UI

It's breaking the standard slightly in that the key should be the MIME type but I did this for now. Hopefully OAPI v3 supports this better.

after do |example|
  example.metadata[:response][:examples] = {
    example.metadata[:example_group][:description] => JSON.parse(response.body, symbolize_names: true)
  }
end

response 422, "invalid parameters" do
  context "field A was missing" do
    let(:params) { { b: "foo" } }
    run_test!
  end

  context "field B was missing" do
    let(:params) { { a: "bar" } }
    run_test!
  end
end

You could try

after do |example|
  example.metadata[:response][:content] = {
    'application/json' => {
      examples: {
        example.metadata[:example_group][:description] => {
          value: JSON.parse(response.body, symbolize_names: true)
        }
      }
    }
  }
end

response 422, "invalid parameters" do
  context "field A was missing" do
    let(:params) { { b: "foo" } }
    run_test!
  end

  context "field B was missing" do
    let(:params) { { a: "bar" } }
    run_test!
  end
end
Was this page helpful?
0 / 5 - 0 ratings