pacto 0.2.5 → 0.3.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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