pacto 0.3.0.pre → 0.3.0

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 (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
@@ -1,25 +0,0 @@
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"
@@ -1,44 +0,0 @@
1
- module Pacto
2
- class << self
3
-
4
- def register_contract(contract = nil, *tags)
5
- tags << :default if tags.empty?
6
- start_count = registered.count
7
- tags.uniq.each do |tag|
8
- registered[tag] << contract
9
- end
10
- registered.count - start_count
11
- end
12
-
13
- def use(tag, values = {})
14
- merged_contracts = registered[:default] + registered[tag]
15
-
16
- raise ArgumentError, "contract \"#{tag}\" not found" if merged_contracts.empty?
17
-
18
- merged_contracts.each do |contract|
19
- contract.stub_contract! values
20
- end
21
- merged_contracts.count
22
- end
23
-
24
- def registered
25
- @registered ||= Hash.new { |hash, key| hash[key] = Set.new }
26
- end
27
-
28
- def unregister_all!
29
- registered.clear
30
- end
31
-
32
- def contract_for(request_signature)
33
- matches = Set.new
34
- registered.values.each do |contract_set|
35
- contract_set.each do |contract|
36
- if contract.matches? request_signature
37
- matches.add contract
38
- end
39
- end
40
- end
41
- matches
42
- end
43
- end
44
- end
@@ -1,14 +0,0 @@
1
- module Pacto
2
- class HashMergeProcessor
3
- def process(response_body, values = {})
4
- unless values.nil? || values.empty?
5
- if response_body.respond_to?(:normalize_keys)
6
- response_body = response_body.normalize_keys.deep_merge(values.normalize_keys)
7
- else
8
- response_body = values
9
- end
10
- end
11
- response_body.to_s
12
- end
13
- end
14
- end
@@ -1,57 +0,0 @@
1
- module Pacto
2
- class Request
3
- attr_reader :host
4
-
5
- def initialize(host, definition)
6
- @host = host
7
- @definition = definition
8
- end
9
-
10
- def method
11
- @definition['method'].to_s.downcase.to_sym
12
- end
13
-
14
- def path
15
- @definition['path']
16
- end
17
-
18
- def headers
19
- @definition['headers']
20
- end
21
-
22
- def params
23
- @definition['params']
24
- end
25
-
26
- def absolute_uri
27
- @host + path
28
- end
29
-
30
- def full_uri
31
- return absolute_uri if params.empty?
32
-
33
- uri = Addressable::URI.new
34
- uri.query_values = params
35
-
36
- absolute_uri + '?' + uri.query
37
- end
38
-
39
- def execute
40
- response = HTTParty.send(method, @host + path, {
41
- httparty_params_key => normalized_params,
42
- :headers => headers
43
- })
44
- ResponseAdapter.new(response)
45
- end
46
-
47
- private
48
-
49
- def httparty_params_key
50
- method == :get ? :query : :body
51
- end
52
-
53
- def normalized_params
54
- method == :get ? params : params.to_json
55
- end
56
- end
57
- end
@@ -1,63 +0,0 @@
1
- module Pacto
2
- class Response
3
- attr_reader :status, :headers, :schema
4
-
5
- def initialize(definition)
6
- @definition = definition
7
- @status = @definition['status']
8
- @headers = @definition['headers']
9
- @schema = @definition['body']
10
- end
11
-
12
- def instantiate
13
- OpenStruct.new({
14
- 'status' => @status,
15
- 'headers' => @headers,
16
- 'body' => JSON::Generator.generate(@schema)
17
- })
18
- end
19
-
20
- def validate(response, opt = {})
21
-
22
- unless opt[:body_only]
23
- if @definition['status'] != response.status
24
- return ["Invalid status: expected #{@definition['status']} but got #{response.status}"]
25
- end
26
-
27
- unless @definition['headers'].normalize_keys.subset_of?(response.headers.normalize_keys)
28
- return ["Invalid headers: expected #{@definition['headers'].inspect} to be a subset of #{response.headers.inspect}"]
29
- end
30
- end
31
-
32
- if @definition['body']
33
- if @definition['body']['type'] && @definition['body']['type'] == 'string'
34
- validate_as_pure_string response.body
35
- else
36
- response.respond_to?(:body) ? validate_as_json(response.body) : validate_as_json(response)
37
- end
38
- else
39
- []
40
- end
41
- end
42
-
43
- private
44
-
45
- def validate_as_pure_string response_body
46
- errors = []
47
- if @definition['body']['required'] && response_body.nil?
48
- errors << 'The response does not contain a body'
49
- end
50
-
51
- pattern = @definition['body']['pattern']
52
- if pattern && !(response_body =~ Regexp.new(pattern))
53
- errors << "The response does not match the pattern #{pattern}"
54
- end
55
-
56
- errors
57
- end
58
-
59
- def validate_as_json response_body
60
- JSON::Validator.fully_validate(@definition['body'], response_body, :version => :draft3)
61
- end
62
- end
63
- end
@@ -1,24 +0,0 @@
1
- module Pacto
2
- class ResponseAdapter
3
- def initialize(response)
4
- @response = response
5
- end
6
-
7
- def status
8
- @response.code
9
- end
10
-
11
- def body
12
- @response.body
13
- end
14
-
15
- def headers
16
- # Normalize headers values according to RFC2616
17
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
18
- normalized_headers = @response.headers.map do |(key, value)|
19
- [key, value.join(',')]
20
- end
21
- Hash[normalized_headers]
22
- end
23
- end
24
- end
@@ -1,57 +0,0 @@
1
- module Pacto
2
- module Stubs
3
- class BuiltIn
4
-
5
- def initialize
6
- register_callbacks
7
- end
8
-
9
- def stub_request! request, response
10
- stub = WebMock.stub_request(request.method, "#{request.host}#{request.path}")
11
- stub = stub.with(request_details(request)) if Pacto.configuration.strict_matchers
12
- stub.to_return({
13
- :status => response.status,
14
- :headers => response.headers,
15
- :body => format_body(response.body)
16
- })
17
- end
18
-
19
- def reset!
20
- WebMock.reset!
21
- WebMock.reset_callbacks
22
- end
23
-
24
- private
25
-
26
- def register_callbacks
27
- WebMock.after_request do |request_signature, response|
28
- contracts = Pacto.contract_for request_signature
29
- Pacto.configuration.callback.process contracts, request_signature, response
30
- end
31
- end
32
-
33
- def format_body(body)
34
- if body.is_a?(Hash) || body.is_a?(Array)
35
- body.to_json
36
- else
37
- body
38
- end
39
- end
40
-
41
- def request_details request
42
- details = {}
43
- unless request.params.empty?
44
- details[webmock_params_key(request)] = request.params
45
- end
46
- unless request.headers.empty?
47
- details[:headers] = request.headers
48
- end
49
- details
50
- end
51
-
52
- def webmock_params_key request
53
- request.method == :get ? :query : :body
54
- end
55
- end
56
- end
57
- end
@@ -1,133 +0,0 @@
1
- describe Pacto do
2
- let(:tag) { 'contract_tag' }
3
- let(:another_tag) { 'another_tag' }
4
- let(:contract) { double('contract') }
5
- let(:another_contract) { double('another_contract') }
6
-
7
- after do
8
- described_class.unregister_all!
9
- end
10
-
11
- describe '.register' do
12
- context 'no tag' do
13
- it 'registers the contract with the default tag' do
14
- described_class.register_contract contract
15
- expect(described_class.registered[:default]).to include(contract)
16
- end
17
- end
18
-
19
- context 'one tag' do
20
- it 'registers a contract under a given tag' do
21
- described_class.register_contract(contract, tag)
22
- expect(described_class.registered[tag]).to include(contract)
23
- end
24
-
25
- it 'does not duplicate a contract when it has already been registered with the same tag' do
26
- described_class.register_contract(contract, tag)
27
- described_class.register_contract(contract, tag)
28
- expect(described_class.registered[tag]).to include(contract)
29
- expect(described_class.registered[tag]).to have(1).items
30
- end
31
- end
32
-
33
- context 'multiple tags' do
34
- it 'registers a contract using different tags' do
35
- described_class.register_contract(contract, tag, another_tag)
36
- expect(described_class.registered[tag]).to include(contract)
37
- expect(described_class.registered[another_tag]).to include(contract)
38
- end
39
-
40
- it 'registers a tag with different contracts ' do
41
- described_class.register_contract(contract, tag)
42
- described_class.register_contract(another_contract, tag)
43
- expect(described_class.registered[tag]).to include(contract, another_contract)
44
- end
45
-
46
- end
47
-
48
- context 'with a block' do
49
- it 'has a compact syntax for registering multiple contracts' do
50
- described_class.configure do |c|
51
- c.register_contract 'new_api/create_item_v2', :item, :new
52
- c.register_contract 'authentication', :default
53
- c.register_contract 'list_items_legacy', :legacy
54
- c.register_contract 'get_item_legacy', :legacy
55
- end
56
- expect(described_class.registered[:new]).to include('new_api/create_item_v2')
57
- expect(described_class.registered[:default]).to include('authentication')
58
- expect(described_class.registered[:legacy]).to include('list_items_legacy', 'get_item_legacy')
59
- end
60
- end
61
- end
62
-
63
- describe '.use' do
64
- before do
65
- described_class.register_contract(contract, tag)
66
- described_class.register_contract(another_contract, :default)
67
- end
68
-
69
- context 'when a contract has been registered' do
70
- let(:response_body) { double('response_body') }
71
-
72
- it 'stubs a contract with default values' do
73
- contract.should_receive(:stub_contract!)
74
- another_contract.should_receive(:stub_contract!)
75
- expect(described_class.use(tag)).to eq 2
76
- end
77
-
78
- it 'stubs default contract if unused tag' do
79
- another_contract.should_receive(:stub_contract!)
80
- expect(described_class.use(another_tag)).to eq 1
81
- end
82
- end
83
-
84
- context 'when contract has not been registered' do
85
- it 'raises an argument error' do
86
- described_class.unregister_all!
87
- expect { described_class.use('unregistered') }.to raise_error ArgumentError
88
- end
89
- end
90
- end
91
-
92
- describe '.unregister_all!' do
93
- it 'unregisters all previously registered contracts' do
94
- described_class.register_contract(contract, tag)
95
- described_class.unregister_all!
96
- expect(described_class.registered).to be_empty
97
- end
98
- end
99
-
100
- describe '.contract_for' do
101
- let(:request_signature) { double('request signature') }
102
-
103
- context 'when no contracts are found for a request' do
104
- it 'returns an empty list' do
105
- expect(described_class.contract_for request_signature).to be_empty
106
- end
107
- end
108
-
109
- context 'when contracts are found for a request' do
110
- let(:contracts_that_match) { create_contracts 2, true }
111
- let(:contracts_that_dont_match) { create_contracts 3, false }
112
- let(:all_contracts) { contracts_that_match + contracts_that_dont_match }
113
-
114
- it 'returns the matching contracts' do
115
- register_and_use all_contracts
116
- expect(described_class.contract_for request_signature).to eq(contracts_that_match)
117
- end
118
- end
119
- end
120
-
121
- def create_contracts(total, matches)
122
- total.times.map do
123
- double('contract',
124
- :stub_contract! => double('request matcher'),
125
- :matches? => matches)
126
- end.to_set
127
- end
128
-
129
- def register_and_use contracts
130
- contracts.each { |contract| described_class.register_contract contract }
131
- Pacto.use :default
132
- end
133
- end
@@ -1,20 +0,0 @@
1
- module Pacto
2
- describe HashMergeProcessor do
3
- describe '#process' do
4
- let(:response_body_string) { 'a simple string' }
5
- let(:response_body_hash) {
6
- {'a' => 'simple hash'}
7
- }
8
-
9
- it 'does not change contract if values is nil' do
10
- expect(subject.process(response_body_string, nil)).to eq response_body_string
11
- end
12
-
13
- it 'merges response body with values' do
14
- merged_body = {'a' => 'simple hash', 'b' => :key}
15
- expect(subject.process(response_body_hash, {:b => :key})).to eq merged_body.to_s
16
- end
17
-
18
- end
19
- end
20
- end