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
@@ -0,0 +1,23 @@
1
+ module Pacto
2
+ describe ERBProcessor do
3
+ subject(:processor) { ERBProcessor.new }
4
+
5
+ describe '#process' do
6
+ let(:erb) { '2 + 2 = <%= 2 + 2 %>' }
7
+ let(:result) { '2 + 2 = 4' }
8
+
9
+ it 'returns the result of ERB' do
10
+ expect(processor.process(erb)).to eq result
11
+ end
12
+
13
+ it 'logs the erb processed' do
14
+ Logger.instance.should_receive(:debug).with("Processed contract: \"#{result}\"")
15
+ processor.process erb
16
+ end
17
+
18
+ it 'does not mess with pure JSONs' do
19
+ processor.process('{"property": ["one", "two, null"]}')
20
+ end
21
+ end
22
+ end
23
+ end
@@ -3,30 +3,30 @@ module Pacto
3
3
  describe HashSubsetOf do
4
4
  describe '#subset_of?' do
5
5
  context 'when the other hash is the same' do
6
- it 'should return true' do
7
- {:a => 'a'}.should be_subset_of({:a => 'a'})
6
+ it 'returns true' do
7
+ expect({:a => 'a'}).to be_subset_of({:a => 'a'})
8
8
  end
9
9
  end
10
10
 
11
11
  context 'when the other hash is a subset' do
12
- it 'should return true' do
13
- {:a => 'a'}.should be_subset_of({:a => 'a', :b => 'b'})
12
+ it 'returns true' do
13
+ expect({:a => 'a'}).to be_subset_of({:a => 'a', :b => 'b'})
14
14
  end
15
15
  end
16
16
 
17
17
  context 'when the other hash is not a subset' do
18
- it 'should return false' do
19
- {:a => 'a'}.subset_of?({:a => 'b'}).should be_false
18
+ it 'returns false' do
19
+ expect({:a => 'a'}.subset_of?({:a => 'b'})).to be_false
20
20
  end
21
21
  end
22
22
  end
23
23
 
24
24
  describe '#normalize_keys' do
25
- it 'should turn keys into downcased strings' do
26
- {:A => 'a'}.normalize_keys.should == {'a' => 'a'}
27
- {:a => 'a'}.normalize_keys.should == {'a' => 'a'}
28
- {'A' => 'a'}.normalize_keys.should == {'a' => 'a'}
29
- {'a' => 'a'}.normalize_keys.should == {'a' => 'a'}
25
+ it 'turns keys into downcased strings' do
26
+ expect({:A => 'a'}.normalize_keys).to eq({'a' => 'a'})
27
+ expect({:a => 'a'}.normalize_keys).to eq({'a' => 'a'})
28
+ expect({'A' => 'a'}.normalize_keys).to eq({'a' => 'a'})
29
+ expect({'a' => 'a'}.normalize_keys).to eq({'a' => 'a'})
30
30
  end
31
31
  end
32
32
  end
@@ -0,0 +1,142 @@
1
+ module Pacto
2
+ describe Generator do
3
+ let(:record_host) {
4
+ 'http://example.com'
5
+ }
6
+ let(:request) do
7
+ Pacto::Request.new(record_host, {
8
+ 'method' => 'GET',
9
+ 'path' => '/abcd',
10
+ 'headers' => {
11
+ 'Content-Length' => [1234],
12
+ 'Via' => ['Some Proxy'],
13
+ 'User-Agent' => ['rspec']
14
+ },
15
+ 'params' => []
16
+ })
17
+ end
18
+ let(:response_adapter) do
19
+ Pacto::ResponseAdapter.new(
20
+ OpenStruct.new({
21
+ 'status' => 200,
22
+ 'headers' => {
23
+ 'Date' => [Time.now],
24
+ 'Server' => ['Fake Server'],
25
+ 'Content-Type' => ['application/json']
26
+ },
27
+ 'body' => double('dummy body')
28
+ })
29
+ )
30
+ end
31
+ let(:response_body_schema) { '{"message": "dummy generated schema"}' }
32
+ let(:version) { 'draft3' }
33
+ let(:schema_generator) { double('schema_generator') }
34
+ let(:validator) { double('validator') }
35
+ let(:request_file) { 'request.json' }
36
+ let(:generator) { described_class.new version, schema_generator, validator }
37
+
38
+ def pretty obj
39
+ MultiJson.encode(obj, :pretty => true).gsub(/^$\n/, '')
40
+ end
41
+
42
+ describe '#generate' do
43
+ let(:request_contract) {
44
+ double({
45
+ :request => request,
46
+ })
47
+ }
48
+ let(:generated_contract) { double('generated contract') }
49
+ before do
50
+ Pacto.should_receive(:build_from_file).with(request_file, record_host).and_return request_contract
51
+ request.should_receive(:execute).and_return response_adapter
52
+ end
53
+
54
+ it 'parses the request' do
55
+ generator.should_receive(:save).with(request_file, request, anything)
56
+ generator.generate request_file, record_host
57
+ end
58
+
59
+ it 'fetches a response' do
60
+ generator.should_receive(:save).with(request_file, anything, response_adapter)
61
+ generator.generate request_file, record_host
62
+ end
63
+
64
+ it 'saves the result' do
65
+ generator.should_receive(:save).with(request_file, request, response_adapter).and_return generated_contract
66
+ expect(generator.generate request_file, record_host).to eq(generated_contract)
67
+ end
68
+ end
69
+
70
+ describe '#save' do
71
+ context 'invalid schema' do
72
+ it 'raises an error if schema generation fails' do
73
+ JSON::SchemaGenerator.should_receive(:generate).and_raise ArgumentError.new('Could not generate schema')
74
+ expect { generator.save request_file, request, response_adapter }.to raise_error
75
+ end
76
+
77
+ it 'raises an error if the generated contract is invalid' do
78
+ JSON::SchemaGenerator.should_receive(:generate).and_return response_body_schema
79
+ validator.should_receive(:validate).and_raise InvalidContract.new('dummy error')
80
+ expect { generator.save request_file, request, response_adapter }.to raise_error
81
+ end
82
+ end
83
+
84
+ context 'valid schema' do
85
+ let(:raw_contract) {
86
+ JSON::SchemaGenerator.should_receive(:generate).with(request_file, response_adapter.body, 'draft3').and_return response_body_schema
87
+ validator.should_receive(:validate).and_return true
88
+ generator.save request_file, request, response_adapter
89
+ }
90
+ subject(:generated_contract) { JSON.parse raw_contract }
91
+
92
+ it 'sets the body to the generated json-schema' do
93
+ expect(subject['response']['body']).to eq(JSON.parse response_body_schema)
94
+ end
95
+
96
+ it 'sets the request attributes' do
97
+ generated_request = subject['request']
98
+ expect(generated_request['params']).to eq(request.params)
99
+ expect(generated_request['path']).to eq(request.path)
100
+ end
101
+
102
+ it 'keeps important request headers' do
103
+ saved_headers = subject['request']['headers']
104
+ expect(saved_headers.keys).to include 'User-Agent'
105
+ end
106
+
107
+ it 'filters informational request headers' do
108
+ saved_headers = subject['request']['headers']
109
+ expect(saved_headers).not_to include 'Date'
110
+ expect(saved_headers).not_to include 'Server'
111
+ expect(saved_headers).not_to include 'Content-Length'
112
+ expect(saved_headers).not_to include 'Connection'
113
+ end
114
+
115
+ it 'normalizes the request method' do
116
+ generated_request = subject['request']
117
+ expect(generated_request['method']).to eq(request.method.downcase.to_s)
118
+ end
119
+
120
+ it 'sets the response attributes' do
121
+ generated_response = subject['response']
122
+ expect(generated_response['status']).to eq(response_adapter.status)
123
+ end
124
+
125
+ it 'keeps important response headers' do
126
+ saved_headers = subject['response']['headers']
127
+ expect(saved_headers.keys).to include 'Content-Type'
128
+ end
129
+
130
+ it 'filters informational response headers' do
131
+ saved_headers = subject['response']['headers']
132
+ expect(saved_headers).not_to include 'Content-Length'
133
+ expect(saved_headers).not_to include 'Via'
134
+ end
135
+
136
+ it 'generates pretty JSON' do
137
+ expect(raw_contract).to eq(pretty(subject))
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,44 @@
1
+ module Pacto
2
+ describe Logger do
3
+ before do
4
+ logger.log logger_lib
5
+ end
6
+
7
+ subject(:logger) { described_class.instance }
8
+ let(:logger_lib) { ::Logger.new(StringIO.new) }
9
+
10
+ it 'delegates debug to the logger lib' do
11
+ logger_lib.should_receive(:debug)
12
+ logger.debug
13
+ end
14
+
15
+ it 'delegates info to the logger lib' do
16
+ logger_lib.should_receive(:info)
17
+ logger.info
18
+ end
19
+
20
+ it 'delegates warn to the logger lib' do
21
+ logger_lib.should_receive(:warn)
22
+ logger.warn
23
+ end
24
+
25
+ it 'delegates error to the logger lib' do
26
+ logger_lib.should_receive(:error)
27
+ logger.error
28
+ end
29
+
30
+ it 'delegates fatal to the logger lib' do
31
+ logger_lib.should_receive(:error)
32
+ logger.error
33
+ end
34
+
35
+ it 'has the default log level as error' do
36
+ expect(logger.level).to eq :error
37
+ end
38
+
39
+ it 'provides access to the log level' do
40
+ logger.level = :info
41
+ expect(logger.level).to eq :info
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,70 @@
1
+ module Pacto
2
+ describe MetaSchema do
3
+ let(:valid_contract) do
4
+ <<-EOF
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
+ EOF
32
+ end
33
+
34
+ let(:invalid_contract) do
35
+ <<-EOF
36
+ {
37
+ "request": {
38
+ "method": "GET",
39
+ "path": "/hello_world",
40
+ "headers": {
41
+ "Accept": "application/json"
42
+ },
43
+ "params": {}
44
+ }
45
+
46
+ }
47
+ EOF
48
+ end
49
+
50
+ subject(:schema) { MetaSchema.new }
51
+
52
+ describe 'when validating a contract against the master schema' do
53
+ context 'with a valid contract structure' do
54
+ it 'does not raise any exceptions' do
55
+ expect {
56
+ schema.validate(valid_contract)
57
+ }.to_not raise_error(Exception)
58
+ end
59
+ end
60
+
61
+ context 'with an invalid contract structure' do
62
+ it 'raises InvalidContract exception' do
63
+ expect {
64
+ schema.validate(invalid_contract)
65
+ }.to raise_error(InvalidContract)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,23 +1,41 @@
1
1
  describe Pacto do
2
- let(:contract_name) { 'contract' }
3
- let(:contract) { double('contract') }
4
2
 
5
- after do
6
- described_class.unregister_all!
3
+ around(:each) do |example|
4
+ $stdout = StringIO.new
5
+ example.run
6
+ $stdout = STDOUT
7
7
  end
8
8
 
9
- describe '.register' do
10
- context 'by default' do
11
- it 'should register a contract under a given name' do
12
- described_class.register(contract_name, contract)
13
- described_class.registered[contract_name].should == contract
9
+ def output
10
+ $stdout.string.strip
11
+ end
12
+
13
+ def mock_validation(errors)
14
+ expect(JSON::Validator).to receive(:fully_validate).with(any_args).and_return errors
15
+ end
16
+
17
+ describe '.validate_contract' do
18
+ context 'valid' do
19
+ it 'displays a success message and return true' do
20
+ mock_validation []
21
+ success = Pacto.validate_contract 'my_contract.json'
22
+ expect(output).to eq 'All contracts successfully meta-validated'
23
+ expect(success).to be_true
14
24
  end
15
25
  end
16
26
 
17
- context 'when a contract has already been registered with the same name' do
18
- it 'should raise an argument error' do
19
- described_class.register(contract_name, contract)
20
- expect { described_class.register(contract_name, contract) }.to raise_error(ArgumentError)
27
+ context 'invalid' do
28
+ it 'displays one error messages and return false' do
29
+ mock_validation ['Error 1']
30
+ success = Pacto.validate_contract 'my_contract.json'
31
+ expect(output).to match /error/
32
+ expect(success).to be_false
33
+ end
34
+
35
+ it 'displays several error messages and return false' do
36
+ mock_validation ['Error 1', 'Error 2']
37
+ success = Pacto.validate_contract 'my_contract.json'
38
+ expect(success).to be_false
21
39
  end
22
40
  end
23
41
  end
@@ -35,52 +53,8 @@ describe Pacto do
35
53
 
36
54
  it 'returns whatever the factory returns' do
37
55
  Pacto::ContractFactory.stub(:build_from_file => instantiated_contract)
38
- described_class.build_from_file(path, host, file_pre_processor).should == instantiated_contract
56
+ expect(described_class.build_from_file(path, host, file_pre_processor)).to eq instantiated_contract
39
57
  end
40
58
  end
41
59
 
42
- describe '.use' do
43
- before do
44
- described_class.register(contract_name, contract)
45
- end
46
-
47
- context 'by default' do
48
- let(:instantiated_contract) { double('instantiated contract', :response_body => response_body)}
49
- let(:response_body) { double('response_body') }
50
-
51
- before do
52
- described_class.registered[contract_name].stub(:instantiate => instantiated_contract)
53
- instantiated_contract.stub(:stub!)
54
- end
55
-
56
- it 'should instantiate a contract with default values' do
57
- described_class.registered[contract_name].should_receive(:instantiate).with(nil).and_return(instantiated_contract)
58
- described_class.use(contract_name)
59
- end
60
-
61
- it 'should return the instantiated contract' do
62
- described_class.use(contract_name).should == instantiated_contract
63
- end
64
-
65
- it 'should stub further requests with the instantiated contract' do
66
- instantiated_contract.should_receive(:stub!)
67
- described_class.use(contract_name)
68
- end
69
-
70
- end
71
-
72
- context 'when contract has not been registered' do
73
- it 'should raise an argument error' do
74
- expect { described_class.use('unregistered') }.to raise_error ArgumentError
75
- end
76
- end
77
- end
78
-
79
- describe '.unregister_all!' do
80
- it 'should unregister all previously registered contracts' do
81
- described_class.register(contract_name, contract)
82
- described_class.unregister_all!
83
- described_class.registered.should be_empty
84
- end
85
- end
86
60
  end