pacto 0.2.5 → 0.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.gitignore +3 -0
  2. data/.rspec +0 -2
  3. data/.rubocop-todo.yml +51 -0
  4. data/.rubocop.yml +1 -0
  5. data/.travis.yml +4 -2
  6. data/Guardfile +28 -14
  7. data/README.md +81 -51
  8. data/Rakefile +24 -11
  9. data/features/generation/generation.feature +25 -0
  10. data/features/journeys/validation.feature +74 -0
  11. data/features/support/env.rb +16 -0
  12. data/lib/pacto.rb +63 -34
  13. data/lib/pacto/contract.rb +25 -11
  14. data/lib/pacto/contract_factory.rb +13 -44
  15. data/lib/pacto/core/callback.rb +11 -0
  16. data/lib/pacto/core/configuration.rb +34 -0
  17. data/lib/pacto/core/contract_repository.rb +44 -0
  18. data/lib/pacto/erb_processor.rb +18 -0
  19. data/lib/pacto/exceptions/invalid_contract.rb +10 -1
  20. data/lib/pacto/extensions.rb +2 -2
  21. data/lib/pacto/generator.rb +75 -0
  22. data/lib/pacto/hash_merge_processor.rb +14 -0
  23. data/lib/pacto/hooks/erb_hook.rb +17 -0
  24. data/lib/pacto/logger.rb +42 -0
  25. data/lib/pacto/meta_schema.rb +17 -0
  26. data/lib/pacto/rake_task.rb +75 -12
  27. data/lib/pacto/request.rb +3 -4
  28. data/lib/pacto/response.rb +27 -19
  29. data/lib/pacto/server.rb +2 -0
  30. data/lib/pacto/server/dummy.rb +45 -0
  31. data/lib/pacto/server/playback_servlet.rb +21 -0
  32. data/lib/pacto/stubs/built_in.rb +57 -0
  33. data/lib/pacto/version.rb +1 -1
  34. data/pacto.gemspec +8 -2
  35. data/resources/contract_schema.json +216 -0
  36. data/spec/coveralls_helper.rb +10 -0
  37. data/spec/integration/data/strict_contract.json +33 -0
  38. data/spec/integration/data/templating_contract.json +25 -0
  39. data/spec/integration/e2e_spec.rb +40 -7
  40. data/spec/integration/templating_spec.rb +55 -0
  41. data/spec/spec_helper.rb +17 -0
  42. data/spec/unit/data/simple_contract.json +22 -0
  43. data/spec/unit/hooks/erb_hook_spec.rb +51 -0
  44. data/spec/unit/pacto/configuration_spec.rb +51 -0
  45. data/spec/unit/pacto/contract_factory_spec.rb +4 -35
  46. data/spec/unit/pacto/contract_spec.rb +59 -31
  47. data/spec/unit/pacto/core/configuration_spec.rb +28 -0
  48. data/spec/unit/pacto/core/contract_repository_spec.rb +133 -0
  49. data/spec/unit/pacto/erb_processor_spec.rb +23 -0
  50. data/spec/unit/pacto/extensions_spec.rb +11 -11
  51. data/spec/unit/pacto/generator_spec.rb +142 -0
  52. data/spec/unit/pacto/hash_merge_processor_spec.rb +20 -0
  53. data/spec/unit/pacto/logger_spec.rb +44 -0
  54. data/spec/unit/pacto/meta_schema_spec.rb +70 -0
  55. data/spec/unit/pacto/pacto_spec.rb +32 -58
  56. data/spec/unit/pacto/request_spec.rb +83 -34
  57. data/spec/unit/pacto/response_adapter_spec.rb +9 -11
  58. data/spec/unit/pacto/response_spec.rb +68 -68
  59. data/spec/unit/pacto/server/playback_servlet_spec.rb +24 -0
  60. data/spec/unit/pacto/stubs/built_in_spec.rb +168 -0
  61. metadata +291 -147
  62. data/.rspec_integration +0 -4
  63. data/.rspec_unit +0 -4
  64. data/lib/pacto/file_pre_processor.rb +0 -12
  65. data/lib/pacto/instantiated_contract.rb +0 -62
  66. data/spec/integration/spec_helper.rb +0 -1
  67. data/spec/integration/utils/dummy_server.rb +0 -34
  68. data/spec/unit/pacto/file_pre_processor_spec.rb +0 -13
  69. data/spec/unit/pacto/instantiated_contract_spec.rb +0 -224
  70. data/spec/unit/spec_helper.rb +0 -5
data/.gitignore CHANGED
@@ -18,3 +18,6 @@ tmp
18
18
  *.swp
19
19
  *.swo
20
20
  .idea/
21
+ .floo*
22
+ .sublime*
23
+
data/.rspec CHANGED
@@ -1,4 +1,2 @@
1
1
  --colour
2
2
  --require spec_helper
3
- --require integration/spec_helper
4
- --require unit/spec_helper
data/.rubocop-todo.yml ADDED
@@ -0,0 +1,51 @@
1
+ # This configuration was generated by `rubocop --auto-gen-config`.
2
+ # The point is for the user to remove these configuration records
3
+ # one by one as the offences are removed from the code base.
4
+
5
+ Blocks:
6
+ Enabled: false
7
+
8
+ CollectionMethods:
9
+ Enabled: false
10
+
11
+ ColonMethodCall:
12
+ Enabled: false
13
+
14
+ DefWithoutParentheses:
15
+ Enabled: false
16
+
17
+ Documentation:
18
+ Enabled: false
19
+
20
+ DotPosition:
21
+ Enabled: false
22
+
23
+ Encoding:
24
+ Enabled: false
25
+
26
+ HashSyntax:
27
+ Enabled: false
28
+
29
+ IfUnlessModifier:
30
+ Enabled: false
31
+
32
+ LineLength:
33
+ Enabled: false
34
+
35
+ MethodAndVariableSnakeCase:
36
+ Enabled: false
37
+
38
+ MethodLength:
39
+ Enabled: false
40
+
41
+ SpaceInsideHashLiteralBraces:
42
+ Enabled: false
43
+
44
+ SymbolName:
45
+ Enabled: false
46
+
47
+ Void:
48
+ Enabled: false
49
+
50
+ WordArray:
51
+ Enabled: false
data/.rubocop.yml ADDED
@@ -0,0 +1 @@
1
+ inherit_from: .rubocop-todo.yml
data/.travis.yml CHANGED
@@ -3,5 +3,7 @@ rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
5
  - jruby
6
- env:
7
- - CI=true
6
+ - 1.8.7
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: 1.8.7
data/Guardfile CHANGED
@@ -1,16 +1,30 @@
1
- guard :rspec do
2
- # Unit tests
3
- watch(%r{^spec/unit/.+_spec\.rb$})
4
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
5
- watch('spec/spec_helper.rb') { "spec/unit" }
6
- watch('spec/unit/spec_helper.rb') { "spec/unit" }
7
- watch(%r{^spec/unit/data/.+\.json$}) { "spec/unit" }
1
+ guard :rubocop, all_on_start: false do
2
+ watch(%r{.+\.rb$})
3
+ watch('.rubocop.yml') { '.' }
4
+ watch('.rubocop-todo.yml') { '.' }
5
+ end
6
+
7
+ group :tests, halt_on_fail: true do
8
+ guard :rspec do
9
+ # Unit tests
10
+ watch(%r{^spec/unit/.+_spec\.rb$})
11
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
12
+ watch('spec/spec_helper.rb') { "spec/unit" }
13
+ watch('spec/unit/spec_helper.rb') { "spec/unit" }
14
+ watch(%r{^spec/unit/data/.+\.json$}) { "spec/unit" }
15
+
16
+ # Integration tests
17
+ watch(%r{^spec/integration/.+_spec\.rb$})
18
+ watch(%r{^spec/integration/utils/.+\.rb$}) { "spec/integration" }
19
+ watch(%r{^lib/.+.rb$}) { "spec/integration" }
20
+ watch('spec/spec_helper.rb') { "spec/integration" }
21
+ watch('spec/integration/spec_helper.rb') { "spec/integration" }
22
+ watch(%r{^spec/integration/data/.+\.json$}) { "spec/integration" }
23
+ end
8
24
 
9
- # Integration tests
10
- watch(%r{^spec/integration/.+_spec\.rb$})
11
- watch(%r{^spec/integration/utils/.+\.rb$}) { "spec/integration" }
12
- watch(%r{^lib/.+.rb$}) { "spec/integration" }
13
- watch('spec/spec_helper.rb') { "spec/integration" }
14
- watch('spec/integration/spec_helper.rb') { "spec/integration" }
15
- watch(%r{^spec/integration/data/.+\.json$}) { "spec/integration" }
25
+ guard :cucumber, all_on_start: false do
26
+ watch(%r{^features/.+\.feature$})
27
+ watch(%r{^features/support/.+$}) { 'features' }
28
+ watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
29
+ end
16
30
  end
data/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ [![Build Status](https://travis-ci.org/thoughtworks/pacto.png)](https://travis-ci.org/thoughtworks/pacto)
2
+ [![Code Climate](https://codeclimate.com/github/thoughtworks/pacto.png)](https://codeclimate.com/github/thoughtworks/pacto)
3
+ [![Dependency Status](https://gemnasium.com/thoughtworks/pacto.png)](https://gemnasium.com/thoughtworks/pacto)
4
+ [![Coverage Status](https://coveralls.io/repos/thoughtworks/pacto/badge.png)](https://coveralls.io/r/thoughtworks/pacto)
5
+
1
6
  # Pacto
2
7
 
3
8
  Pacto is a Ruby implementation of the [Consumer-Driven Contracts](http://martinfowler.com/articles/consumerDrivenContracts.html)
@@ -32,32 +37,34 @@ A response has the following attributes:
32
37
  Pacto relies on a simple, JSON based language for defining contracts. Below is an example contract for a GET request
33
38
  to the /hello_world endpoint of a provider:
34
39
 
35
- {
36
- "request": {
37
- "method": "GET",
38
- "path": "/hello_world",
39
- "headers": {
40
- "Accept": "application/json"
41
- },
42
- "params": {}
43
- },
44
-
45
- "response": {
46
- "status": 200,
47
- "headers": {
48
- "Content-Type": "application/json"
49
- },
50
- "body": {
51
- "description": "A simple response",
52
- "type": "object",
53
- "properties": {
54
- "message": {
55
- "type": "string"
56
- }
57
- }
40
+ ```json
41
+ {
42
+ "request": {
43
+ "method": "GET",
44
+ "path": "/hello_world",
45
+ "headers": {
46
+ "Accept": "application/json"
47
+ },
48
+ "params": {}
49
+ },
50
+
51
+ "response": {
52
+ "status": 200,
53
+ "headers": {
54
+ "Content-Type": "application/json"
55
+ },
56
+ "body": {
57
+ "description": "A simple response",
58
+ "type": "object",
59
+ "properties": {
60
+ "message": {
61
+ "type": "string"
58
62
  }
59
63
  }
60
64
  }
65
+ }
66
+ }
67
+ ```
61
68
 
62
69
  The host address is intentionally left out of the request specification so that we can validate a contract against any provider.
63
70
  It also reinforces the fact that a contract defines the expectation of a consumer, and not the implementation of any specific provider.
@@ -68,13 +75,23 @@ There are two ways to validate a contract against a provider: through a Rake tas
68
75
 
69
76
  ### Rake Task
70
77
 
71
- Pacto includes a default Rake task. To use it, include it in your Rakefile:
78
+ Pacto includes two Rake tasks. In order to use them, include this in your Rakefile:
79
+
80
+ ```ruby
81
+ require 'pacto/rake_task'
82
+ ```
72
83
 
73
- require 'pacto/rake_task'
84
+ Pacto can validate the contract files:
74
85
 
75
- Validating a contract against a provider is as simple as running:
86
+ ```sh
87
+ $ rake pacto:meta_validate[dir] # Validates a directory of contract definitions
88
+ ```
76
89
 
77
- $ rake pacto:validate[host,dir] # Validates all contracts in a given directory against a given host
90
+ Or it can validate contracts against a provider:
91
+
92
+ ```sh
93
+ $ rake pacto:validate[host,dir] # Validates all contracts in a given directory against a given host
94
+ ```
78
95
 
79
96
  It is recommended that you also include [colorize](https://github.com/fazibear/colorize) to get prettier, colorful output.
80
97
 
@@ -82,11 +99,27 @@ It is recommended that you also include [colorize](https://github.com/fazibear/c
82
99
 
83
100
  The easiest way to load a contract from a file and validate it against a host is by using the builder interface:
84
101
 
85
- require 'pacto'
102
+ ```ruby
103
+ require 'pacto'
104
+
105
+ WebMock.allow_net_connect!
106
+ contract = Pacto.build_from_file('/path/to/contract.json', 'http://dummyprovider.com')
107
+ contract.validate
108
+ ```
109
+
110
+ If you don't want to depend on Pacto to do the request you can also validate a response from a real request:
86
111
 
87
- WebMock.allow_net_connect!
88
- contract = Pacto.build_from_file('/path/to/contract.json', 'http://dummyprovider.com')
89
- contract.validate
112
+ ```ruby
113
+ require 'pacto'
114
+
115
+ WebMock.allow_net_connect!
116
+ contract = Pacto.build_from_file('/path/to/contract.json', 'http://dummyprovider.com')
117
+ # Doing the request with ruby stdlib, you can use your favourite lib to do the request
118
+ response = Net::HTTP.get_response(URI.parse('http://dummyprovider.com')).body
119
+ contract.validate response, body_only: true
120
+ ```
121
+
122
+ Pacto also has the ability to match a request signature to a contract that is currently in used, via ```Pacto.contract_for request_signature```
90
123
 
91
124
  ## Auto-Generated Stubs
92
125
 
@@ -95,31 +128,28 @@ to generate a valid JSON document as the response body, and relies on [WebMock](
95
128
  to stub any HTTP requests made by your application. Important: the JSON generator is in very early stages and does not work
96
129
  with the entire JSON Schema specification.
97
130
 
98
- First, register the contracts that are going to be used in the acceptance tests suite:
99
-
100
- require 'pacto'
101
-
102
- contract = Pacto.build_from_file('/path/to/contract.json', 'http://dummyprovider.com')
103
- Pacto.register('my_contract', contract)
104
-
131
+ First, register the contracts that are going to be used in the acceptance tests suite. The register_contract method accepts zero or more tags:
132
+ ```ruby
133
+ require 'pacto'
134
+
135
+ contract1 = Pacto.build_from_file('/path/to/contract1.json', 'http://dummyprovider.com')
136
+ contract2 = Pacto.build_from_file('/path/to/contract2.json', 'http://dummyprovider.com')
137
+ contract3 = Pacto.build_from_file('/path/to/contract3.json', 'http://dummyprovider.com')
138
+ Pacto.register_contract(contract1)
139
+ Pacto.register_contract(contract2, :public_api)
140
+ Pacto.register_contract(contract3, :public_api, :wip)
141
+ ```
105
142
  Then, in the setup phase of the test, specify which contracts will be used for that test:
106
-
107
- Pacto.use('my_contract')
108
-
143
+ ```ruby
144
+ Pacto.use('my_tag')
145
+ ```
109
146
  If default values are not specified in the contract's response body, a default value will be automatically generated. It is possible
110
147
  to overwrite those values, however, by passing a second argument:
111
-
112
- Pacto.use('my_contract', :value => 'new value')
113
-
148
+ ```ruby
149
+ Pacto.use('my_tag', :value => 'new value')
150
+ ```
114
151
  The values are merged using [hash-deep-merge](https://github.com/Offirmo/hash-deep-merge).
115
152
 
116
- ## Code status
117
-
118
- [![Build Status](https://travis-ci.org/thoughtworks/pacto.png)](https://travis-ci.org/thoughtworks/pacto)
119
- [![Code Climate](https://codeclimate.com/github/thoughtworks/pacto.png)](https://codeclimate.com/github/thoughtworks/pacto)
120
- [![Dependency Status](https://gemnasium.com/thoughtworks/pacto.png)](https://gemnasium.com/thoughtworks/pacto)
121
- [![Coverage Status](https://coveralls.io/repos/thoughtworks/pacto/badge.png)](https://coveralls.io/r/thoughtworks/pacto)
122
-
123
153
  ## Contributing
124
154
 
125
155
  1. Fork it
data/Rakefile CHANGED
@@ -1,16 +1,29 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
+ require 'pacto/rake_task'
4
+ require 'cucumber'
5
+ require 'cucumber/rake/task'
6
+ require 'coveralls/rake/task'
7
+ require 'rubocop/rake_task'
3
8
 
4
- if defined?(RSpec)
5
- desc "Run unit tests"
6
- task :unit do
7
- abort unless system('rspec --option .rspec_unit')
8
- end
9
+ Coveralls::RakeTask.new
9
10
 
10
- desc "Run integration tests"
11
- task :integration do
12
- abort unless system('rspec --option .rspec_integration')
13
- end
11
+ Rubocop::RakeTask.new(:rubocop) do |task|
12
+ task.patterns = ['**/*.rb', 'Rakefile']
13
+ # abort rake on failure
14
+ task.fail_on_error = false
15
+ end
16
+
17
+ Cucumber::Rake::Task.new(:journeys) do |t|
18
+ t.cucumber_opts = 'features --format pretty'
19
+ end
14
20
 
15
- task :default => [:unit, :integration]
21
+ RSpec::Core::RakeTask.new(:unit) do |t|
22
+ t.pattern = 'spec/unit/**/*_spec.rb'
16
23
  end
24
+
25
+ RSpec::Core::RakeTask.new(:integration) do |t|
26
+ t.pattern = 'spec/integration/**/*_spec.rb'
27
+ end
28
+
29
+ task :default => [:unit, :integration, :journeys, :rubocop, 'coveralls:push']
@@ -0,0 +1,25 @@
1
+ @needs_server
2
+ Feature: Contract Generation
3
+ Scenario: Generating a contract from a partial contract
4
+ Given a directory named "contracts"
5
+ Given a file named "requests/my_contract.json" with:
6
+ """
7
+ {
8
+ "request": {
9
+ "method": "GET",
10
+ "path": "/hello",
11
+ "headers": {
12
+ "Accept": "application/json"
13
+ },
14
+ "params": {}
15
+ },
16
+ "response": {
17
+ "status": 200,
18
+ "body": {
19
+ "required": true
20
+ }
21
+ }
22
+ }
23
+ """
24
+ When I successfully run `bundle exec rake --trace pacto:generate['tmp/aruba/requests','tmp/aruba/contracts','http://localhost:8000']`
25
+ Then the output should contain "Successfully generated all contracts"
@@ -0,0 +1,74 @@
1
+ Feature: Validation journey
2
+ Scenario: Meta-validation of a valid contract
3
+ Given a file named "contracts/my_contract.json" with:
4
+ """
5
+ {
6
+ "request": {
7
+ "method": "GET",
8
+ "path": "/hello_world",
9
+ "headers": {
10
+ "Accept": "application/json"
11
+ },
12
+ "params": {}
13
+ },
14
+
15
+ "response": {
16
+ "status": 200,
17
+ "headers": {
18
+ "Content-Type": "application/json"
19
+ },
20
+ "body": {
21
+ "description": "A simple response",
22
+ "type": "object",
23
+ "properties": {
24
+ "message": {
25
+ "type": "string"
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ """
32
+ When I successfully run `bundle exec rake pacto:meta_validate['tmp/aruba/contracts/my_contract.json']`
33
+ Then the output should contain "All contracts successfully meta-validated"
34
+
35
+
36
+ Scenario: Meta-validation of an invalid contract
37
+ Given a file named "contracts/my_contract.json" with:
38
+ """
39
+ {"request": "yes"}
40
+ """
41
+ When I run `bundle exec rake pacto:meta_validate['tmp/aruba/contracts/my_contract.json']`
42
+ Then the exit status should be 1
43
+ And the output should contain "did not match the following type"
44
+
45
+
46
+ Scenario: Meta-validation of a contract with empty request and response
47
+ Given a file named "contracts/my_contract.json" with:
48
+ """
49
+ {"request": {}, "response": {}}
50
+ """
51
+ When I run `bundle exec rake pacto:meta_validate['tmp/aruba/contracts/my_contract.json']`
52
+ Then the exit status should be 1
53
+ And the output should contain "did not contain a required property"
54
+
55
+ Scenario: Meta-validation of a contracts response body
56
+ Given a file named "contracts/my_contract.json" with:
57
+ """
58
+ {
59
+ "request": {
60
+ "method": "GET",
61
+ "path": "/hello_world"
62
+ },
63
+
64
+ "response": {
65
+ "status": 200,
66
+ "body": {
67
+ "required": "anystring"
68
+ }
69
+ }
70
+ }
71
+ """
72
+ When I run `bundle exec rake pacto:meta_validate['tmp/aruba/contracts/my_contract.json']`
73
+ Then the exit status should be 1
74
+ And the output should contain "did not match the following type"
@@ -0,0 +1,16 @@
1
+ require_relative '../../spec/coveralls_helper'
2
+ require 'rake'
3
+ require 'aruba'
4
+ require 'aruba/cucumber'
5
+ require 'aruba/in_process'
6
+ require 'aruba/jruby' if RUBY_PLATFORM == 'java'
7
+ require 'pacto/server'
8
+
9
+ Before do
10
+ @aruba_timeout_seconds = RUBY_PLATFORM == 'java' ? 60 : 10
11
+ end
12
+
13
+ Before('@needs_server') do
14
+ @server = Pacto::Server::Dummy.new 8000, '/hello', '{"message": "Hello World!"}'
15
+ @server.start
16
+ end