pacto 0.3.0.pre → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rubocop-todo.yml +0 -27
  4. data/.rubocop.yml +9 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +4 -5
  8. data/CONTRIBUTING.md +112 -0
  9. data/Gemfile +5 -0
  10. data/Guardfile +18 -13
  11. data/README.md +157 -101
  12. data/Rakefile +3 -3
  13. data/features/configuration/strict_matchers.feature +97 -0
  14. data/features/evolve/README.md +11 -0
  15. data/features/evolve/existing_services.feature +82 -0
  16. data/features/generate/README.md +5 -0
  17. data/features/generate/generation.feature +28 -0
  18. data/features/steps/pacto_steps.rb +75 -0
  19. data/features/stub/README.md +2 -0
  20. data/features/stub/templates.feature +46 -0
  21. data/features/support/env.rb +11 -5
  22. data/features/validate/README.md +1 -0
  23. data/features/validate/body_only.feature +85 -0
  24. data/features/{journeys/validation.feature → validate/meta_validation.feature} +41 -24
  25. data/features/validate/validation.feature +36 -0
  26. data/lib/pacto.rb +61 -33
  27. data/lib/pacto/contract.rb +18 -15
  28. data/lib/pacto/contract_factory.rb +14 -11
  29. data/lib/pacto/contract_files.rb +17 -0
  30. data/lib/pacto/contract_list.rb +17 -0
  31. data/lib/pacto/contract_validator.rb +29 -0
  32. data/lib/pacto/core/configuration.rb +19 -17
  33. data/lib/pacto/core/contract_registry.rb +43 -0
  34. data/lib/pacto/core/{callback.rb → hook.rb} +3 -3
  35. data/lib/pacto/core/modes.rb +33 -0
  36. data/lib/pacto/core/validation_registry.rb +45 -0
  37. data/lib/pacto/erb_processor.rb +0 -1
  38. data/lib/pacto/extensions.rb +18 -4
  39. data/lib/pacto/generator.rb +34 -49
  40. data/lib/pacto/generator/filters.rb +41 -0
  41. data/lib/pacto/hooks/erb_hook.rb +4 -3
  42. data/lib/pacto/logger.rb +4 -2
  43. data/lib/pacto/meta_schema.rb +4 -2
  44. data/lib/pacto/rake_task.rb +28 -25
  45. data/lib/pacto/request_clause.rb +43 -0
  46. data/lib/pacto/request_pattern.rb +8 -0
  47. data/lib/pacto/response_clause.rb +15 -0
  48. data/lib/pacto/rspec.rb +102 -0
  49. data/lib/pacto/stubs/uri_pattern.rb +23 -0
  50. data/lib/pacto/stubs/webmock_adapter.rb +69 -0
  51. data/lib/pacto/stubs/webmock_helper.rb +71 -0
  52. data/lib/pacto/ui.rb +7 -0
  53. data/lib/pacto/uri.rb +9 -0
  54. data/lib/pacto/validation.rb +57 -0
  55. data/lib/pacto/validators/body_validator.rb +41 -0
  56. data/lib/pacto/validators/request_body_validator.rb +23 -0
  57. data/lib/pacto/validators/response_body_validator.rb +23 -0
  58. data/lib/pacto/validators/response_header_validator.rb +49 -0
  59. data/lib/pacto/validators/response_status_validator.rb +24 -0
  60. data/lib/pacto/version.rb +1 -1
  61. data/pacto.gemspec +33 -29
  62. data/resources/contract_schema.json +8 -176
  63. data/resources/draft-03.json +174 -0
  64. data/spec/integration/data/strict_contract.json +2 -2
  65. data/spec/integration/e2e_spec.rb +22 -31
  66. data/spec/integration/rspec_spec.rb +94 -0
  67. data/spec/integration/templating_spec.rb +9 -12
  68. data/{lib → spec}/pacto/server.rb +0 -0
  69. data/{lib → spec}/pacto/server/dummy.rb +11 -8
  70. data/{lib → spec}/pacto/server/playback_servlet.rb +1 -1
  71. data/spec/spec_helper.rb +2 -0
  72. data/spec/unit/hooks/erb_hook_spec.rb +15 -15
  73. data/spec/unit/pacto/configuration_spec.rb +2 -10
  74. data/spec/unit/pacto/contract_factory_spec.rb +16 -13
  75. data/spec/unit/pacto/contract_files_spec.rb +42 -0
  76. data/spec/unit/pacto/contract_list_spec.rb +35 -0
  77. data/spec/unit/pacto/contract_spec.rb +43 -44
  78. data/spec/unit/pacto/contract_validator_spec.rb +85 -0
  79. data/spec/unit/pacto/core/configuration_spec.rb +4 -11
  80. data/spec/unit/pacto/core/contract_registry_spec.rb +119 -0
  81. data/spec/unit/pacto/core/modes_spec.rb +18 -0
  82. data/spec/unit/pacto/core/validation_registry_spec.rb +76 -0
  83. data/spec/unit/pacto/core/validation_spec.rb +60 -0
  84. data/spec/unit/pacto/extensions_spec.rb +14 -23
  85. data/spec/unit/pacto/generator/filters_spec.rb +99 -0
  86. data/spec/unit/pacto/generator_spec.rb +34 -73
  87. data/spec/unit/pacto/meta_schema_spec.rb +46 -6
  88. data/spec/unit/pacto/pacto_spec.rb +17 -15
  89. data/spec/unit/pacto/{request_spec.rb → request_clause_spec.rb} +32 -44
  90. data/spec/unit/pacto/request_pattern_spec.rb +22 -0
  91. data/spec/unit/pacto/response_clause_spec.rb +54 -0
  92. data/spec/unit/pacto/stubs/uri_pattern_spec.rb +28 -0
  93. data/spec/unit/pacto/stubs/webmock_adapter_spec.rb +205 -0
  94. data/spec/unit/pacto/stubs/webmock_helper_spec.rb +20 -0
  95. data/spec/unit/pacto/uri_spec.rb +20 -0
  96. data/spec/unit/pacto/validators/body_validator_spec.rb +105 -0
  97. data/spec/unit/pacto/validators/response_header_validator_spec.rb +94 -0
  98. data/spec/unit/pacto/validators/response_status_validator_spec.rb +20 -0
  99. metadata +230 -146
  100. data/features/generation/generation.feature +0 -25
  101. data/lib/pacto/core/contract_repository.rb +0 -44
  102. data/lib/pacto/hash_merge_processor.rb +0 -14
  103. data/lib/pacto/request.rb +0 -57
  104. data/lib/pacto/response.rb +0 -63
  105. data/lib/pacto/response_adapter.rb +0 -24
  106. data/lib/pacto/stubs/built_in.rb +0 -57
  107. data/spec/unit/pacto/core/contract_repository_spec.rb +0 -133
  108. data/spec/unit/pacto/hash_merge_processor_spec.rb +0 -20
  109. data/spec/unit/pacto/response_adapter_spec.rb +0 -25
  110. data/spec/unit/pacto/response_spec.rb +0 -201
  111. data/spec/unit/pacto/stubs/built_in_spec.rb +0 -168
data/Rakefile CHANGED
@@ -5,17 +5,17 @@ require 'cucumber'
5
5
  require 'cucumber/rake/task'
6
6
  require 'coveralls/rake/task'
7
7
  require 'rubocop/rake_task'
8
+ require 'rake/notes/rake_task'
8
9
 
9
10
  Coveralls::RakeTask.new
10
11
 
11
12
  Rubocop::RakeTask.new(:rubocop) do |task|
12
- task.patterns = ['**/*.rb', 'Rakefile']
13
13
  # abort rake on failure
14
- task.fail_on_error = false
14
+ task.fail_on_error = true
15
15
  end
16
16
 
17
17
  Cucumber::Rake::Task.new(:journeys) do |t|
18
- t.cucumber_opts = 'features --format pretty'
18
+ t.cucumber_opts = 'features --format progress'
19
19
  end
20
20
 
21
21
  RSpec::Core::RakeTask.new(:unit) do |t|
@@ -0,0 +1,97 @@
1
+ Feature: Strict Matching
2
+
3
+ By default, Pacto matches requests to contracts (and stubs) using exact request paths, parameters, and headers. This strict behavior is useful for Consumer-Driven Contracts.
4
+
5
+ You can use less strict matching so the same contract can match multiple similar requests. This is useful for matching contracts with resource identifiers in the path. Any placeholder in the path (like :id in /beers/:id) is considered a resource identifier and will match any alphanumeric combination.
6
+
7
+ You can change the default behavior to the behavior that allows placeholders and ignores headers or request parameters by setting the strict_matchers configuration option:
8
+
9
+ ```ruby
10
+ Pacto.configure do |config|
11
+ config.strict_matchers = false
12
+ end
13
+ ```
14
+
15
+ Background:
16
+ Given a file named "contracts/hello_contract.json" with:
17
+ """json
18
+ {
19
+ "request": {
20
+ "method": "GET",
21
+ "path": "/hello/:id",
22
+ "headers": {
23
+ "Accept": "application/json"
24
+ },
25
+ "params": {}
26
+ },
27
+
28
+ "response": {
29
+ "status": 200,
30
+ "headers": { "Content-Type": "application/json" },
31
+ "body": {
32
+ "type": "object",
33
+ "required": true,
34
+ "properties": {
35
+ "message": { "type": "string", "required": true, "default": "Hello, world!" }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ """
41
+
42
+ Given a file named "requests.rb" with:
43
+ """ruby
44
+ require 'pacto'
45
+
46
+ strict = ARGV[0] == "true"
47
+ puts "Pacto.configuration.strict_matchers = #{strict}"
48
+ puts
49
+
50
+ Pacto.configure do |config|
51
+ config.strict_matchers = strict
52
+ end
53
+ Pacto.load_contracts('contracts', 'http://dummyprovider.com').stub_all
54
+
55
+ def response url, headers
56
+ begin
57
+ response = Faraday.get(url) do |req|
58
+ req.headers = headers[:headers]
59
+ end
60
+ response.body
61
+ rescue WebMock::NetConnectNotAllowedError => e
62
+ e.class
63
+ end
64
+ end
65
+
66
+ print 'Exact: '
67
+ puts response 'http://dummyprovider.com/hello/:id', headers: {'Accept' => 'application/json' }
68
+
69
+ print 'Wrong headers: '
70
+ puts response 'http://dummyprovider.com/hello/:id', headers: {'Content-Type' => 'application/json' }
71
+
72
+ print 'ID placeholder: '
73
+ puts response 'http://dummyprovider.com/hello/123', headers: {'Accept' => 'application/json' }
74
+ """
75
+
76
+ Scenario: Default (strict) behavior
77
+ When I run `bundle exec ruby requests.rb true`
78
+ Then the output should contain:
79
+ """
80
+ Pacto.configuration.strict_matchers = true
81
+
82
+ Exact: {"message":"Hello, world!"}
83
+ Wrong headers: WebMock::NetConnectNotAllowedError
84
+ ID placeholder: WebMock::NetConnectNotAllowedError
85
+
86
+ """
87
+
88
+ Scenario: Non-strict matching
89
+ When I run `bundle exec ruby requests.rb false`
90
+ Then the output should contain:
91
+ """
92
+ Pacto.configuration.strict_matchers = false
93
+
94
+ Exact: {"message":"Hello, world!"}
95
+ Wrong headers: {"message":"Hello, world!"}
96
+ ID placeholder: {"message":"Hello, world!"}
97
+ """
@@ -0,0 +1,11 @@
1
+ ## Consumer-Driven Contract Recommendations
2
+
3
+ If you are using Pacto for Consumer-Driven Contracts, we recommend avoiding the advanced features so you'll test with the strictest Contract possible. We recommend:
4
+
5
+ - Do not use templating, let Pacto use the json-generator
6
+ - Use strict request matching
7
+ - Use multiple contracts for the same service to capture attributes that are required in some situations but not others
8
+
9
+ The host address is intentionally left out of the request specification so that we can validate a contract against any provider.
10
+ It also reinforces the fact that a contract defines the expectation of a consumer, and not the implementation of any specific provider.
11
+
@@ -0,0 +1,82 @@
1
+ Feature: Existing services journey
2
+
3
+ Scenario: Generating the contracts
4
+ Given I have a Rakefile
5
+ Given an existing set of services
6
+ When I execute:
7
+ """ruby
8
+ require 'pacto'
9
+
10
+ Pacto.configure do | c |
11
+ c.contracts_path = 'contracts'
12
+ end
13
+
14
+ Pacto.generate!
15
+
16
+ Faraday.get 'http://www.example.com/service1'
17
+ Faraday.get 'http://www.example.com/service2'
18
+ """
19
+ Then the following files should exist:
20
+ | contracts/www.example.com/service1.json |
21
+ | contracts/www.example.com/service2.json |
22
+
23
+ @no-clobber
24
+ Scenario: Ensuring all contracts are valid
25
+ When I successfully run `bundle exec rake pacto:meta_validate['contracts']`
26
+ Then the output should contain exactly:
27
+ """
28
+ Validating contracts/www.example.com/service1.json
29
+ Validating contracts/www.example.com/service2.json
30
+ All contracts successfully meta-validated
31
+
32
+ """
33
+
34
+ @no-clobber
35
+ Scenario: Stubbing with the contracts
36
+ Given a file named "test.rb" with:
37
+ """ruby
38
+ require 'pacto'
39
+
40
+ Pacto.configure do | c |
41
+ c.contracts_path = 'contracts'
42
+ end
43
+
44
+ contracts = Pacto.load_contracts('contracts/www.example.com/', 'http://www.example.com')
45
+ contracts.stub_all
46
+
47
+ puts Faraday.get('http://www.example.com/service1').body
48
+ puts Faraday.get('http://www.example.com/service2').body
49
+ """
50
+ When I successfully run `bundle exec ruby test.rb`
51
+ Then the output should contain exactly:
52
+ """
53
+ {"thoughtworks":"bar"}
54
+ {"service2":["bar"]}
55
+
56
+ """
57
+
58
+ @no-clobber
59
+ Scenario: Expecting a change
60
+ When I make replacements in "contracts/www.example.com/service1.json":
61
+ | pattern | replacement |
62
+ | string | integer |
63
+ When I execute:
64
+ """ruby
65
+ require 'pacto'
66
+
67
+ Pacto.stop_generating!
68
+
69
+ Pacto.configure do | c |
70
+ c.contracts_path = 'contracts'
71
+ end
72
+
73
+ Pacto.load_contracts('contracts', 'http://www.example.com').stub_all
74
+ Pacto.validate!
75
+
76
+ Faraday.get 'http://www.example.com/service1'
77
+ Faraday.get 'http://www.example.com/service2'
78
+ """
79
+ Then the output should contain exactly:
80
+ """
81
+
82
+ """
@@ -0,0 +1,5 @@
1
+ We know - json-schema can get pretty verbose! It's a powerful tool, but writing the entire Contract by hand for a complex service is a painstaking task. We've created a simple generator to speed this process up. You can invoke it programmatically, or with the provided rake task.
2
+
3
+ The basic generate we've bundled with Pacto completes partially defined Contracts - Contracts that have a request defined but no response. We haven't bundled any other generates, but you could use the API to generate from other sources, like existing [VCR](https://github.com/vcr/vcr) cassettes, [apiblueprint](http://apiblueprint.org/), or [WADL](https://wadl.java.net/). If you want some help or ideas, try the [pacto mailing-list](https://groups.google.com/forum/#!forum/pacto-gem).
4
+
5
+ Note: Request headers are only recorded if they are in the response's [Vary header](http://www.subbu.org/blog/2007/12/vary-header-for-restful-applications), so make sure your services return a proper Vary for best results!
@@ -0,0 +1,28 @@
1
+ @needs_server
2
+ Feature: Contract Generation
3
+
4
+ We know - json-schema can get pretty verbose! It's a powerful tool, but writing the entire Contract by hand for a complex service is a painstaking task. We've created a simple generator to speed this process up. You can invoke it programmatically, or with the provided rake task.
5
+
6
+ You just need to create a partial Contract that only describes the request. The generator will then execute the request, and use the response to generate a full Contract.
7
+
8
+ Remember, we only record request headers if they are in the response's [Vary header](http://www.subbu.org/blog/2007/12/vary-header-for-restful-applications), so make sure your services return a proper Vary for best results!
9
+
10
+ Background:
11
+ Given a file named "requests/my_contract.json" with:
12
+ """
13
+ {
14
+ "request": {
15
+ "method": "GET",
16
+ "path": "/hello",
17
+ "headers": {
18
+ "Accept": "application/json"
19
+ }
20
+ },
21
+ "response": {
22
+ "status": 200,
23
+ "body": {
24
+ "required": true
25
+ }
26
+ }
27
+ }
28
+ """
@@ -0,0 +1,75 @@
1
+ Given(/^Pacto is configured with:$/) do |string|
2
+ steps %Q{
3
+ Given a file named "pacto_config.rb" with:
4
+ """ruby
5
+ #{string}
6
+ """
7
+ }
8
+ end
9
+
10
+ Given(/^I have a Rakefile$/) do
11
+ steps %Q{
12
+ Given a file named "Rakefile" with:
13
+ """ruby
14
+ require 'pacto/rake_task'
15
+ """
16
+ }
17
+ end
18
+
19
+ When(/^I request "(.*?)"$/) do |url|
20
+ steps %Q{
21
+ Given a file named "request.rb" with:
22
+ """ruby
23
+ require 'pacto'
24
+ require_relative 'pacto_config'
25
+ require 'faraday'
26
+
27
+ resp = Faraday.get('#{url}') do |req|
28
+ req.headers = { 'Accept' => 'application/json' }
29
+ end
30
+ puts resp.body
31
+ """
32
+ When I run `bundle exec ruby request.rb`
33
+ }
34
+ end
35
+
36
+ Given(/^an existing set of services$/) do
37
+ WebMock.stub_request(:get, 'www.example.com/service1').to_return(:body => {'thoughtworks' => 'pacto' }.to_json)
38
+ WebMock.stub_request(:post, 'www.example.com/service1').with(:body => 'thoughtworks').to_return(:body => 'pacto')
39
+ WebMock.stub_request(:get, 'www.example.com/service2').to_return(:body => {'service2' => %w{'thoughtworks', 'pacto'} }.to_json)
40
+ WebMock.stub_request(:post, 'www.example.com/service2').with(:body => 'thoughtworks').to_return(:body => 'pacto')
41
+ end
42
+
43
+ When(/^I execute:$/) do |script|
44
+ FileUtils.mkdir_p 'tmp/aruba'
45
+ Dir.chdir 'tmp/aruba' do
46
+ begin
47
+ script = <<-eof
48
+ require 'stringio'
49
+ begin $stdout = StringIO.new
50
+ #{ script }
51
+ $stdout.string
52
+ ensure
53
+ $stdout = STDOUT
54
+ end
55
+ eof
56
+ eval(script) # rubocop:disable Eval
57
+ # It's just for testing...
58
+
59
+ rescue SyntaxError => e
60
+ puts e
61
+ puts e.backtrace
62
+ end
63
+ end
64
+ end
65
+
66
+ When(/^I make replacements in "([^"]*)":$/) do |file_name, replacements|
67
+ Dir.chdir 'tmp/aruba' do
68
+ content = File.read file_name
69
+ replacements.rows.each do | pattern, replacement |
70
+ content.gsub! Regexp.new(pattern), replacement
71
+ end
72
+
73
+ File.open(file_name, 'w') { |file| file.write content }
74
+ end
75
+ end
@@ -0,0 +1,2 @@
1
+ You can write your own stubs and use Pacto to [validate](https://www.relishapp.com/maxlinc/pacto/docs/validate) them, or you can just let Pacto create stubs for you.
2
+
@@ -0,0 +1,46 @@
1
+ Feature: Templating
2
+
3
+ If you want to create more dynamic stubs, you can use Pacto templating. Currently the only supported templating mechanism is to use ERB in the "default" attributes of the json-schema.
4
+
5
+ Background:
6
+ Given Pacto is configured with:
7
+ """ruby
8
+ Pacto.configure do |c|
9
+ c.register_hook Pacto::Hooks::ERBHook.new
10
+ end
11
+ Pacto.load_contracts('contracts', 'http://example.com').stub_all
12
+ """
13
+ Given a file named "contracts/template.json" with:
14
+ """json
15
+ {
16
+ "request": {
17
+ "method": "GET",
18
+ "path": "/hello",
19
+ "headers": {
20
+ "Accept": "application/json"
21
+ },
22
+ "params": {}
23
+ },
24
+
25
+ "response": {
26
+ "status": 200,
27
+ "headers": { "Content-Type": "application/json" },
28
+ "body": {
29
+ "type": "object",
30
+ "required": true,
31
+ "properties": {
32
+ "message": { "type": "string", "required": true,
33
+ "default": "<%= 'Hello, world!'.reverse %>"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ """
40
+
41
+ Scenario: ERB Template
42
+ When I request "http://example.com/hello"
43
+ Then the output should contain:
44
+ """
45
+ {"message":"!dlrow ,olleH"}
46
+ """
@@ -1,16 +1,22 @@
1
1
  require_relative '../../spec/coveralls_helper'
2
2
  require 'rake'
3
+ require 'webmock'
3
4
  require 'aruba'
4
5
  require 'aruba/cucumber'
5
- require 'aruba/in_process'
6
6
  require 'aruba/jruby' if RUBY_PLATFORM == 'java'
7
- require 'pacto/server'
7
+ require_relative '../../spec/pacto/server'
8
8
 
9
9
  Before do
10
+ # Given I successfully run `bundle install` can take a while.
10
11
  @aruba_timeout_seconds = RUBY_PLATFORM == 'java' ? 60 : 10
11
12
  end
12
13
 
13
- Before('@needs_server') do
14
- @server = Pacto::Server::Dummy.new 8000, '/hello', '{"message": "Hello World!"}'
15
- @server.start
14
+ # Was only going to use for @needs_server, but its easier to leave it running
15
+ @server = Pacto::Server::Dummy.new 8000, '/hello', '{"message": "Hello World!"}'
16
+ @server.start
17
+
18
+ Around do | scenario, block |
19
+ Bundler.with_clean_env do
20
+ block.call
21
+ end
16
22
  end
@@ -0,0 +1 @@
1
+ You can use Pacto to do Integration Contract Testing - making sure your service and any test double that simulates the service are similar. If you generate your Contracts from documentation, you can be fairly confident that all three - live services, test doubles, and documentation - are in sync.
@@ -0,0 +1,85 @@
1
+ Feature: Validation
2
+
3
+ You can validate just the body of the contract. This may be useful if you want to validate a stubbing system that does not stub the fully connection, or if Pacto is currently unable to validate your headers.
4
+
5
+ Background:
6
+ Given a file named "Gemfile" with:
7
+ """ruby
8
+ source 'https://rubygems.org'
9
+
10
+ gem 'pacto', :path => '../..'
11
+ gem 'excon'
12
+ """
13
+ Given a file named "validate.rb" with:
14
+ """ruby
15
+ require 'pacto'
16
+ require_relative 'my_service'
17
+
18
+ contract_list = Pacto.load_contracts('contracts', 'http://example.com')
19
+ contract_list.stub_all
20
+
21
+ contract = contract_list.contracts.first
22
+ service = MyService.new
23
+ response = service.hello
24
+ successful = contract.validate_consumer nil, response, :body_only => true
25
+ puts "Validated successfully!" if successful
26
+ """
27
+ Given a file named "contracts/template.json" with:
28
+ """json
29
+ {
30
+ "request": {
31
+ "method": "GET",
32
+ "path": "/hello",
33
+ "headers": {
34
+ "Accept": "application/json"
35
+ },
36
+ "params": {}
37
+ },
38
+
39
+ "response": {
40
+ "status": 200,
41
+ "headers": { "Content-Type": "application/json" },
42
+ "body": {
43
+ "type": "object",
44
+ "required": true,
45
+ "properties": {
46
+ "message": { "type": "string", "required": true
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ """
53
+
54
+ Scenario: Validate the response body only
55
+ # This should be in the Before block, but https://github.com/cucumber/cucumber/issues/52
56
+ Given I successfully run `bundle install --local`
57
+ Given a file named "my_service.rb" with:
58
+ """ruby
59
+ require 'excon'
60
+ class MyService
61
+ def response(params={})
62
+ body = params[:body] || {}
63
+ status = params[:status] || 200
64
+ headers = params[:headers] || {}
65
+
66
+ response = Excon::Response.new(:body => body, :headers => headers, :status => status)
67
+ if params.has_key?(:expects) && ![*params[:expects]].include?(response.status)
68
+ raise(Excon::Errors.status_error(params, response))
69
+ else response
70
+ end
71
+ end
72
+
73
+ def hello
74
+ body = {
75
+ 'message' => 'Hi!'
76
+ }
77
+ response({:body => body})
78
+ end
79
+ end
80
+ """
81
+ When I run `bundle exec ruby validate.rb`
82
+ Then the output should contain:
83
+ """
84
+ Validated successfully!
85
+ """