0

I want to test an API client performing REST requests. The requests look like this:

# vcr/attachments.yml

- method: POST
- path: http://example.org/attachments
- body: { "filename": "foo.jpg", "signature": "6g33jk2C1QQn9EM8Q==" }
- response: 200 OK

- method: POST
- path: http://example.org/attachments
- body: { "filename": "bar.jpg", "signature": "7z44g6aPPk2C17Xf5==" }
- response: 409 Conflict

I'm trying to mock these requests using VCR. In the relevant test, I write:

VCR.use_cassette('attachments', match_requests_on: [:host, :path, :body_as_json]) do
  my_record.attach_all(['foo.jpg', 'bar.jpg'])
  assert_nil     my_record.errors['foo.jpg'] # should succeed with 200
  assert_present my_record.errors['bar.jpg'] # should fail with 409
end

The only thing differentiating the two requests in the "filename"="foo.jpg" body parameter, so I need to match on the request body.

But the problem is that the signature parameter is essentially random - or at least cannot be consistently be predicted (e.g. it changes on the CI server). So matching on the whole body is flaky and unreliable.

How can I ensure that VCR will match the proper recorded request, even when the body will never match perfectly?

Kemenaran
  • 1,421
  • 12
  • 11

1 Answers1

0

Of course, the easiest solution would be to just split the cassette in two. Then you can match them one after another:

VCR.use_cassette('attachment_foo', match_requests_on: [:host, :path]) do
  my_record.attach_all(['foo.jpg'])
  assert_nil my_record.errors['foo.jpg'] # should succeed with 200
end

VCR.use_cassette('attachment_bar', match_requests_on: [:host, :path]) do
  my_record.attach_all(['bar.jpg'])
  assert_present my_record.errors['bar.jpg'] # should fail with 409
end

But if for some reason you can't split the requests (maybe because they need to be batched together), that won't work.

In that case, a more elaborated solution would be to use a custom VCR matcher.

# Match the JSON body of the request, ignoring some specified keys.
def body_as_json_excluding(*keys)
  lambda do |r1, r2|
    begin
      keys = keys.map(&:to_s)
      JSON.parse(r1.body).except(*keys) == JSON.parse(r2.body).except(*keys)
    rescue JSON::ParserError
      false
    end
  end
end

(See up-to-date gist here: https://gist.github.com/kemenaran/2dcc463fdda3e476983bf5500f3524b9)

In your tests, you can then tell VCR to ignore the signature part of the JSON body:

VCR.use_cassette('attachments', match_requests_on:[
    :host,
    :path,
    body_as_json_excluding(:signature)
  ]) do
  my_record.attach_all(['foo.jpg', 'bar.jpg'])
  assert_nil     my_record.errors['foo.jpg'] # should succeed with 200
  assert_present my_record.errors['bar.jpg'] # should fail with 409
end

VCR will now match against the whole JSON, except the part that may change unpredictably.

Kemenaran
  • 1,421
  • 12
  • 11