contracts_api_test 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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