contracts_api_test 0.0.1

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 (49) hide show
  1. data/.gitignore +20 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/Guardfile +8 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +117 -0
  7. data/Rakefile +7 -0
  8. data/TODO.md +19 -0
  9. data/contracts.gemspec +29 -0
  10. data/lib/contracts.rb +46 -0
  11. data/lib/contracts/contract.rb +18 -0
  12. data/lib/contracts/extensions.rb +18 -0
  13. data/lib/contracts/instantiated_contract.rb +63 -0
  14. data/lib/contracts/rake_task.rb +75 -0
  15. data/lib/contracts/request.rb +62 -0
  16. data/lib/contracts/response.rb +27 -0
  17. data/lib/contracts/response_adapter.rb +24 -0
  18. data/lib/contracts/version.rb +3 -0
  19. data/lib/json-generator.rb +1 -0
  20. data/lib/json/generator.rb +18 -0
  21. data/lib/json/generator/array_attribute.rb +11 -0
  22. data/lib/json/generator/attribute_factory.rb +18 -0
  23. data/lib/json/generator/basic_attribute.rb +17 -0
  24. data/lib/json/generator/boolean_attribute.rb +7 -0
  25. data/lib/json/generator/dereferencer.rb +22 -0
  26. data/lib/json/generator/empty_attribute.rb +7 -0
  27. data/lib/json/generator/integer_attribute.rb +7 -0
  28. data/lib/json/generator/object_attribute.rb +18 -0
  29. data/lib/json/generator/string_attribute.rb +7 -0
  30. data/spec/contracts/contract_spec.rb +50 -0
  31. data/spec/contracts/contracts_spec.rb +77 -0
  32. data/spec/contracts/extensions_spec.rb +34 -0
  33. data/spec/contracts/instantiated_contract_spec.rb +224 -0
  34. data/spec/contracts/request_spec.rb +73 -0
  35. data/spec/contracts/response_adapter_spec.rb +27 -0
  36. data/spec/contracts/response_spec.rb +114 -0
  37. data/spec/data/contract.json +25 -0
  38. data/spec/json/generator/array_attribute_spec.rb +42 -0
  39. data/spec/json/generator/attribute_factory_spec.rb +72 -0
  40. data/spec/json/generator/basic_attribute_spec.rb +41 -0
  41. data/spec/json/generator/boolean_attribute_spec.rb +17 -0
  42. data/spec/json/generator/dereferencer_spec.rb +72 -0
  43. data/spec/json/generator/empty_attribute_spec.rb +17 -0
  44. data/spec/json/generator/integer_attribute_spec.rb +17 -0
  45. data/spec/json/generator/object_attribute_spec.rb +100 -0
  46. data/spec/json/generator/string_attribute_spec.rb +17 -0
  47. data/spec/json/generator_spec.rb +20 -0
  48. data/spec/spec_helper.rb +1 -0
  49. metadata +259 -0
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swp
19
+ *.swo
20
+ .idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in contracts.gemspec
4
+ gemspec
@@ -0,0 +1,8 @@
1
+ guard 'rspec', :cli => '--color --require spec_helper', :version => 2 do
2
+ watch(%r{^spec/contracts/.+_spec\.rb$})
3
+ watch(%r{^spec/json/.+_spec\.rb$})
4
+ watch(%r{^lib/contracts\.rb$}) { |m| "spec" }
5
+ watch(%r{^lib/json-generator\.rb$}) { |m| "spec" }
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,117 @@
1
+ # Contracts
2
+
3
+ Contracts is a Ruby implementation of the [Consumer-Driven Contracts](http://martinfowler.com/articles/consumerDrivenContracts.html)
4
+ pattern for evolving services. It's main features are:
5
+
6
+ - A simple language for specifying a contract;
7
+ - An automated way to validate that a producer meets it's consumers requirements;
8
+ - An auto-generated stub to be used in the consumer's acceptance tests.
9
+
10
+ It was developed in a micro-services environment, specifically a RESTful one, so expect it to be opinionated. Although
11
+ there is enough functionality implemented to motivate us to open-source this, it is still a work in progress and under active
12
+ development. Check the Constraints session for further information on what works and what doesn't.
13
+
14
+ ## Specifying Contracts
15
+
16
+ A contract specifies a single message exchange between a consumer and a provider. In a RESTful world, this means
17
+ an HTTP interaction, which is composed of two main parts: a request and a response.
18
+
19
+ A request has the following attributes:
20
+
21
+ - Method: the method of the HTTP request (e.g. GET, POST, PUT, DELETE);
22
+ - Path: the relative path (without host) of the provider's endpoint;
23
+ - Headers: headers sent in the HTTP request;
24
+ - Params: any data or parameters of the HTTP request (e.g. query string for GET, body for POST)
25
+
26
+ A response has the following attributes:
27
+
28
+ - Status: the HTTP response status code (e.g. 200, 404, 500);
29
+ - Headers: the HTTP response headers;
30
+ - Body: a JSON Schema defining the expected structure of the HTTP response body.
31
+
32
+ Contracts relies on a simple, JSON based language for defining contracts. Below is an example contract for a GET request
33
+ to the /hello_world endpoint of a provider:
34
+
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
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ The host address is intentionally left out of the request specification so that we can validate a contract against any provider.
63
+ It also reinforces the fact that a contract defines the expectation of a consumer, and not the implementation of any specific provider.
64
+
65
+ ## Validating Contracts
66
+
67
+ There are two ways to validate a contract against a provider: through a Rake task or programatically.
68
+
69
+ ### Rake Task
70
+
71
+ Contracts includes a default Rake task. To use it, include it in your Rakefile:
72
+
73
+ require 'contracts/rake_task'
74
+
75
+ Validating a contract against a provider is as simple as running:
76
+
77
+ $ rake contracts:validate[host,dir] # Validates all contracts in a given directory against a given host
78
+
79
+ It is recommended that you also include [colorize](https://github.com/fazibear/colorize) to get prettier, colorful output.
80
+
81
+ ### Programatically
82
+
83
+ The easiest way to load a contract from a file and validate it against a host is by using the builder interface:
84
+
85
+ > contract = Contracts.build_from_file('/path/to/contract.json', 'http://dummyprovider.com')
86
+ > contract.validate
87
+
88
+ ## Auto-Generated Stubs
89
+
90
+ Contracts provides an API to be used in the consumer's acceptance tests. It uses a custom JSON Schema parser and generator
91
+ to generate a valid JSON document as the response body, and relies on [WebMock](https://github.com/bblimke/webmock)
92
+ to stub any HTTP requests made by your application. Important: the JSON generator is in very early stages and does not work
93
+ with the entire JSON Schema specification.
94
+
95
+ First, register the contracts that are going to be used in the acceptance tests suite:
96
+
97
+ contract = Contracts.build_from_file('/path/to/contract.json', 'http://dummyprovider.com')
98
+ Contracts.register('my_contract', contract)
99
+
100
+ Then, in the setup phase of the test, specify which contracts will be used for that test:
101
+
102
+ Contracts.use('my_contract')
103
+
104
+ If default values are not specified in the contract's response body, a default value will be automatically generated. It is possible
105
+ to overwrite those values, however, by passing a second argument:
106
+
107
+ Contracts.use('my_contract', :value => 'new value')
108
+
109
+ The values are merged using [hash-deep-merge](https://github.com/Offirmo/hash-deep-merge).
110
+
111
+ ## Contributing
112
+
113
+ 1. Fork it
114
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
115
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
116
+ 4. Push to the branch (`git push origin my-new-feature`)
117
+ 5. Create new Pull Request
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ if defined?(RSpec)
5
+ RSpec::Core::RakeTask.new('spec')
6
+ task :default => :spec
7
+ end
data/TODO.md ADDED
@@ -0,0 +1,19 @@
1
+ # TODO
2
+
3
+ ## Nice to have
4
+
5
+ - Cucumber Tests as docs (see https://relishapp.com/cucumber/cucumber/docs/);
6
+ - Fake Server (sinatra app generating fake responses based on the contracts);
7
+ - Optional "require" format for JSON Schema: # 'required': ['id', 'categorias', 'titulo', ...];
8
+ - Contract variables for easy writing. Such as: 'path': '/member/{id}';
9
+ - Add JSHint rake task to validate contracts syntax;
10
+ - Pretty output for hash difference (using something like [hashdiff](https://github.com/liufengyun/hashdiff)).
11
+ - A default header in the response marking the response as "mocked"
12
+ - Parameter matcher should use an idea of "subset" instead of matching all the parameters
13
+ - 'default' value to be used when it is present with an array of types
14
+ - Support 'null' attribute type
15
+ - Validate contract structure in a rake task. Then assume all contracts are valid.
16
+
17
+ ## Assumptions
18
+
19
+ - JSON Schema references are stored in the 'definitions' attribute, in the schema's root element.
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'contracts/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "contracts_api_test"
8
+ gem.version = Contracts::VERSION
9
+ gem.authors = ["Abril Midia", "ThoughtWorks"]
10
+ gem.email = ["vejasp-dev@abril.com.br", "abril_vejasp_dev@thoughtworks.com"]
11
+ gem.description = %q{Consumer-Driven Contracts}
12
+ gem.summary = %q{Consumer-Driven Contracts Gem}
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency "webmock"
20
+ gem.add_dependency "json"
21
+ gem.add_dependency "json-schema"
22
+ gem.add_dependency "hash-deep-merge"
23
+ gem.add_dependency "httparty"
24
+ gem.add_dependency "addressable"
25
+
26
+ gem.add_development_dependency "rake"
27
+ gem.add_development_dependency "rspec"
28
+ gem.add_development_dependency "guard-rspec"
29
+ end
@@ -0,0 +1,46 @@
1
+ require "contracts/version"
2
+
3
+ require "httparty"
4
+ require "hash_deep_merge"
5
+ require "json"
6
+ require "json-schema"
7
+ require "json-generator"
8
+ require "webmock"
9
+ require "ostruct"
10
+
11
+ require "contracts/extensions"
12
+ require "contracts/request"
13
+ require "contracts/response_adapter"
14
+ require "contracts/response"
15
+ require "contracts/instantiated_contract"
16
+ require "contracts/contract"
17
+
18
+ module Contracts
19
+ def self.build_from_file(contract_path, host)
20
+ definition = JSON.parse(File.read(contract_path))
21
+ request = Request.new(host, definition["request"])
22
+ response = Response.new(definition["response"])
23
+ Contract.new(request, response)
24
+ end
25
+
26
+ def self.register(name, contract)
27
+ raise ArgumentError, "contract \" #{name}\" has already been registered" if registered.has_key?(name)
28
+ registered[name] = contract
29
+ end
30
+
31
+ def self.use(contract_name, values = nil, path = nil)
32
+ raise ArgumentError, "contract \"#{contract_name}\" not found" unless registered.has_key?(contract_name)
33
+ instantiated_contract = registered[contract_name].instantiate(values)
34
+ instantiated_contract.request.path = path unless path.nil?
35
+ instantiated_contract.stub!
36
+ instantiated_contract
37
+ end
38
+
39
+ def self.registered
40
+ @registered ||= {}
41
+ end
42
+
43
+ def self.unregister_all!
44
+ @registered = {}
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ module Contracts
2
+ class Contract
3
+ def initialize(request, response)
4
+ @request = request
5
+ @response = response
6
+ end
7
+
8
+ def instantiate(values = nil)
9
+ instantiated_contract = InstantiatedContract.new(@request, @response.instantiate)
10
+ instantiated_contract.replace!(values) unless values.nil?
11
+ instantiated_contract
12
+ end
13
+
14
+ def validate
15
+ @response.validate(@request.execute)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Contracts
2
+ module Extensions
3
+ module HashSubsetOf
4
+ def subset_of?(other)
5
+ (self.to_a - other.to_a).empty?
6
+ end
7
+
8
+ def normalize_keys
9
+ self.inject({}) do |normalized, (key, value)|
10
+ normalized[key.to_s.downcase] = value
11
+ normalized
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Hash.send(:include, Contracts::Extensions::HashSubsetOf)
@@ -0,0 +1,63 @@
1
+ module Contracts
2
+ class InstantiatedContract
3
+ attr_accessor :request
4
+ attr_reader :response_body
5
+
6
+ def initialize(request, response)
7
+ @request = request
8
+ @response = response
9
+ @response_body = response.body
10
+ end
11
+
12
+ def request_path
13
+ @request.absolute_uri
14
+ end
15
+
16
+ def request_uri
17
+ @request.full_uri
18
+ end
19
+
20
+ def replace!(values)
21
+ if @response_body.respond_to?(:normalize_keys)
22
+ @response_body = @response_body.normalize_keys.deep_merge(values.normalize_keys)
23
+ else
24
+ @response_body = values
25
+ end
26
+ end
27
+
28
+ def stub!
29
+ WebMock.stub_request(@request.method, "#{@request.host}#{@request.path}").
30
+ with(request_details).
31
+ to_return({
32
+ :status => @response.status,
33
+ :headers => @response.headers,
34
+ :body => format_body(@response_body)
35
+ })
36
+ end
37
+
38
+ private
39
+
40
+ def format_body(body)
41
+ if body.is_a?(Hash) or body.is_a?(Array)
42
+ body.to_json
43
+ else
44
+ body
45
+ end
46
+ end
47
+
48
+ def request_details
49
+ details = {}
50
+ unless @request.params.empty?
51
+ details[webmock_params_key] = @request.params
52
+ end
53
+ unless @request.headers.empty?
54
+ details[:headers] = @request.headers
55
+ end
56
+ details
57
+ end
58
+
59
+ def webmock_params_key
60
+ @request.method == :get ? :query : :body
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,75 @@
1
+ require 'contracts'
2
+
3
+ unless String.respond_to?(:colors)
4
+ class String
5
+ def colorize(*args)
6
+ self
7
+ end
8
+ end
9
+ end
10
+
11
+ module Contracts
12
+ class RakeTask
13
+ include Rake::DSL
14
+
15
+ def initialize
16
+ @exit_with_error = false
17
+ end
18
+
19
+ def install
20
+ desc "Tasks for contracts gem"
21
+ namespace :contracts do
22
+ validate_task
23
+ end
24
+ end
25
+
26
+ def validate_task
27
+ desc "Validates all contracts in a given directory against a given host"
28
+ task :validate, :host, :dir do |t, args|
29
+ if args.to_a.size < 2
30
+ fail "USAGE: rake contracts:validate[<host>, <contract_dir>]".colorize(:yellow)
31
+ end
32
+
33
+ validate_contracts(args[:host], args[:dir])
34
+ end
35
+ end
36
+
37
+ def validate_contracts(host, dir)
38
+ WebMock.allow_net_connect!
39
+
40
+ contracts = Dir[File.join(dir, '*.json')]
41
+ if contracts.empty?
42
+ fail "No contracts found in directory #{dir}".colorize(:yellow)
43
+ end
44
+
45
+ puts "Validating contracts in directory #{dir} against host #{host}\n\n"
46
+
47
+ total_failed = 0
48
+ contracts.each do |contract_file|
49
+ print "#{contract_file.split('/').last}:"
50
+ contract = Contracts.build_from_file(contract_file, host)
51
+ errors = contract.validate
52
+
53
+ if errors.empty?
54
+ puts " OK!".colorize(:green)
55
+ else
56
+ @exit_with_error = true
57
+ total_failed += 1
58
+ puts " FAILED!".colorize(:red)
59
+ errors.each do |error|
60
+ puts "\t* #{error}".colorize(:light_red)
61
+ end
62
+ puts ""
63
+ end
64
+ end
65
+
66
+ if @exit_with_error
67
+ fail "#{total_failed} of #{contracts.size} failed. Check output for detailed error messages.".colorize(:red)
68
+ else
69
+ puts "#{contracts.size} valid contract#{contracts.size > 1 ? 's' : nil}".colorize(:green)
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ Contracts::RakeTask.new.install