probity 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZjRlOGFjYmFjZWY3ODdjOTk0MTNlMzJmZDY1OTJiZjI0NjFhNjI1MA==
5
+ data.tar.gz: !binary |-
6
+ MTk3OWY0ZGZiY2FhNWFkNjU5MzZjZmI5MGM4Yzc5NzlhMDgyMjk1MA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ODc2NTIzZTViMzRjOTQwYWY1NWUyNzNkMmJkZGRkZTFmYzBkODVmYzFjMzYw
10
+ ODJjYjM2ZjlkYTU4NzhhMTczZDI1Njg5NGM1MTk2YjVmMWEyMTE0OWNkYWY0
11
+ ZWMxNWQ2ODhlZGE0MWRiY2JhNDJlZDZjMzg1NDk1NjhhZTg5ZmM=
12
+ data.tar.gz: !binary |-
13
+ OTkxOWI1NzI2ZGUyYmVhMTZmZDc5Y2VhOTgzZWM3MzQzN2FjNjNiNmE3NDVk
14
+ NjhiYmYzNzU4YzY2NmJlNzVjMGQ5MjE1YTYyOGY5YjY3YmJiODE3Mzc2ZDNi
15
+ M2RlZDhhMGVjNWVmMDIzOWE4NDI3MmY5MmZiMmMxZDBhMTNkYzQ=
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in probity.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Medidata Solutions Worldwide
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,71 @@
1
+ # Probity
2
+
3
+ Even Rails can't be trusted not to produce malformed xml sometimes. Add this Rack middleware to the stack while you run your tests and it will monitor the responses you send, infer the appropriate validations based on content type, and raise if they fail.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'probity', git: 'git@github.com:mdsol/probity.git'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install probity
20
+
21
+ ## Usage
22
+
23
+ Probity should only be included in your middleware stack when running in test/development mode. If you're using a conventional Rails app, for example, you could include it like so:
24
+
25
+ ```ruby
26
+ # in config/environments/test.rb
27
+ config.middleware.use(Probity::ResponseValidatorMiddleware)
28
+ ```
29
+
30
+ Now, run your test scripts! If your app produces malformed responses, Probity will turn a passing test into a failing one by raising a parsing error in your server. Depending on your particular test setup, this might manifest in different ways, but as long as your tests are at all meaningful they at least shouldn't pass.
31
+
32
+ ## Configuration
33
+
34
+ By default, Probity will validate the `application/xml` and `application/json` content types, and **raise** if it sees anything else. If you're serving something other than those two, such as html, you'll probably want to do
35
+
36
+ ```ruby
37
+ middleware.use(Probity::ResponseValidatorMiddleware, missing_validator: :warn)
38
+ #or
39
+ middleware.use(Probity::ResponseValidatorMiddleware, missing_validator: :ignore)
40
+ ```
41
+
42
+ A blank body is not valid JSON or XML. If you would like to special-case this as an allowable response, use
43
+
44
+ ```ruby
45
+ middleware.use(Probity::ResponseValidatorMiddleware, blank_body: :ignore)
46
+ ```
47
+
48
+ You can add validators for your content types with code like this:
49
+
50
+ ```ruby
51
+ Probity.validators['application/hal+json'] = Proc.new do |body|
52
+ raise unless Halidator.new(body).valid?
53
+ end
54
+ ```
55
+ You can also override the default validators the same way:
56
+
57
+ ```ruby
58
+ Probity.validators['application/xml'] = # Object that responds to .call(str)
59
+ ```
60
+
61
+ ## Validators
62
+
63
+ The XML validator uses [Nokogiri](http://www.nokogiri.org/) in Strict Mode, while the JSON validator uses Ruby's built-in `JSON.parse`. Pull requests are welcome for more content types.
64
+
65
+ ## Contributing
66
+
67
+ 1. Fork it ( https://github.com/[my-github-username]/probity/fork )
68
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
69
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
70
+ 4. Push to the branch (`git push origin my-new-feature`)
71
+ 5. Create a new Pull Request
@@ -0,0 +1,12 @@
1
+ require 'probity/version'
2
+ require 'probity/middleware'
3
+
4
+ module Probity
5
+ def self.validators
6
+ @validators ||= {}
7
+ end
8
+ end
9
+
10
+ unless ENV['PROBITY_NO_AUTOLOAD']
11
+ Gem.find_files('probity/validators/*.rb').each { |f| require(f) }
12
+ end
@@ -0,0 +1,58 @@
1
+ module Probity
2
+
3
+ class ResponseValidatorMiddleware
4
+
5
+ def initialize(app, options = {})
6
+ @app = app
7
+ @options = options
8
+ end
9
+
10
+ def call(env)
11
+ status, headers, response = @app.call(env)
12
+ content_type = response.respond_to?(:content_type) ? response.content_type : headers["Content-Type"]
13
+ validator = Probity.validators[content_type]
14
+ if validator
15
+ body = response.respond_to?(:body) ? response.body : response.join("")
16
+ validate(body, validator)
17
+ else
18
+ missing_validator(content_type)
19
+ end
20
+ [status, headers, response]
21
+ end
22
+
23
+ def validate(body, validator)
24
+ if blank_string?(body)
25
+ blank_body(body, validator)
26
+ else
27
+ validator.call(body)
28
+ end
29
+ end
30
+
31
+ def blank_string?(str)
32
+ ! str[/\S/]
33
+ end
34
+
35
+ def blank_body(body, validator)
36
+ case @options[:blank_body]
37
+ when nil, :validate
38
+ validator.call(body)
39
+ when :raise
40
+ raise 'Response with an empty body'
41
+ when :ignore
42
+ end
43
+ end
44
+
45
+ def missing_validator(content_type)
46
+ error = "No validator defined for #{content_type}. Fix this by putting `#{self.class}.validators['#{content_type}'] = Proc.new do |body| ... end` in a test initializer."
47
+ case @options[:missing_validator]
48
+ when nil, :raise
49
+ raise error
50
+ when :warn
51
+ warn(error)
52
+ when :ignore
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,5 @@
1
+ require 'json'
2
+
3
+ Probity.validators['application/json'] = Proc.new do |body|
4
+ JSON.parse(body)
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'nokogiri'
2
+
3
+ Probity.validators['application/xml'] = Proc.new do |body|
4
+ Nokogiri::XML(body) { |config| config.options = Nokogiri::XML::ParseOptions::STRICT}
5
+ end
@@ -0,0 +1,3 @@
1
+ module Probity
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'probity/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'probity'
8
+ spec.version = Probity::VERSION
9
+ spec.authors = ['Aaron Weiner', 'Curtis White']
10
+ spec.email = spec.authors.map{|name|name.sub(/(.).* (.*)/,'\1\2@mdsol.com')}
11
+ spec.summary = %q{Rack middleware to test whether your app is producing valid json, xml, etc.}
12
+ spec.description = %q{Even Rails can't be trusted not to produce malformed xml sometimes. Add this Rack middleware to the stack while you run your tests and it will monitor the responses you send, infer the appropriate validations based on content type, and raise if they fail.}
13
+ spec.homepage = 'https://github.com/mdsol/probity'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.7'
22
+ spec.add_development_dependency 'rspec'
23
+
24
+ spec.add_dependency 'nokogiri'
25
+
26
+ end
@@ -0,0 +1,141 @@
1
+ require 'probity'
2
+
3
+ describe(Probity::ResponseValidatorMiddleware) do
4
+
5
+ context 'missing a validator for the content type' do
6
+
7
+ before do
8
+ @inner_app = double
9
+ @response = double
10
+ allow(@response).to receive(:content_type).and_return('magic')
11
+ allow(@inner_app).to receive(:call).and_return([200, {}, @response])
12
+ end
13
+
14
+ it 'raises by default' do
15
+ app = described_class.new(@inner_app)
16
+ expect{app.call({})}.to raise_error(RuntimeError, /No validator defined for magic/)
17
+ end
18
+
19
+ it 'warns when passed {missing_validator: :warn}' do
20
+ app = described_class.new(@inner_app, missing_validator: :warn)
21
+ expect_any_instance_of(Object).to receive(:warn).once
22
+ expect{app.call({})}.not_to raise_error
23
+ end
24
+
25
+ it 'does nothing when passed ignore' do
26
+ app = described_class.new(@inner_app, missing_validator: :ignore)
27
+ expect_any_instance_of(Object).not_to receive(:warn)
28
+ expect{app.call({})}.not_to raise_error
29
+ end
30
+
31
+ context "with an array of json strings as the body" do
32
+ before do
33
+ @body = [
34
+ JSON.pretty_generate( {'errors' => {'authentication' => ['Unauthorized']}} ),
35
+ JSON.pretty_generate( {'warning' => "warning"} )
36
+ ]
37
+
38
+ allow(@response).to receive(:content_type).and_return('magic')
39
+ allow(@response).to receive(:body).and_return(@body)
40
+ allow(@inner_app).to receive(:call).and_return([401, {'Content-Type' => 'magic'}, @body])
41
+ end
42
+
43
+ it 'raises by default' do
44
+ app = described_class.new(@inner_app)
45
+ expect{app.call({})}.to raise_error(RuntimeError, /No validator defined for magic/)
46
+ end
47
+
48
+ it 'warns when passed {missing_validator: :warn}' do
49
+ app = described_class.new(@inner_app, missing_validator: :warn)
50
+ expect_any_instance_of(Object).to receive(:warn).once
51
+ expect{app.call({})}.not_to raise_error
52
+ end
53
+
54
+ it 'does nothing when passed ignore' do
55
+ app = described_class.new(@inner_app, missing_validator: :ignore)
56
+ expect_any_instance_of(Object).not_to receive(:warn)
57
+ expect{app.call({})}.not_to raise_error
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+
64
+ context 'with an appropriate validator' do
65
+
66
+ before do
67
+ @inner_app = double
68
+ @response = double
69
+ @body = 'some_body'
70
+ allow(@response).to receive(:content_type).and_return('application/json')
71
+ allow(@response).to receive(:body).and_return(@body)
72
+ allow(@inner_app).to receive(:call).and_return([201,{'foo' => 'bar'},@response])
73
+ end
74
+
75
+ it 'calls the appropriate validator' do
76
+ expect(Probity.validators['application/json']).to receive(:call).with(@body)
77
+ expect(Probity.validators['application/xml']).not_to receive(:call)
78
+ app = described_class.new(@inner_app)
79
+ app.call({})
80
+ end
81
+
82
+ it 'passes the response through transparently if no error is raised by the validator' do
83
+ allow(Probity.validators['application/json']).to receive(:call).and_return(nil)
84
+ app = described_class.new(@inner_app)
85
+ expect(app.call({})).to eq([201,{'foo' => 'bar'},@response])
86
+ end
87
+
88
+ context 'with an empty body' do
89
+
90
+ before do
91
+ @inner_app = double
92
+ @response = double
93
+ @body = ''
94
+ allow(@response).to receive(:content_type).and_return('application/json')
95
+ allow(@response).to receive(:body).and_return(@body)
96
+ allow(@inner_app).to receive(:call).and_return([201,{'foo' => 'bar'},@response])
97
+ end
98
+
99
+ it 'validates as normal by default' do
100
+ expect(Probity.validators['application/json']).to receive(:call).with(@body)
101
+ app = described_class.new(@inner_app)
102
+ app.call({})
103
+ end
104
+
105
+ it 'raises when passed {blank_body: :raise}' do
106
+ expect(Probity.validators['application/json']).not_to receive(:call)
107
+ app = described_class.new(@inner_app, blank_body: :raise)
108
+ expect{app.call({})}.to raise_error(/Response with an empty body/)
109
+ end
110
+
111
+ it 'skips validation when passed {blank_body: :ignore}' do
112
+ expect(Probity.validators['application/json']).not_to receive(:call)
113
+ app = described_class.new(@inner_app, blank_body: :ignore)
114
+ expect{app.call({})}.not_to raise_error
115
+ end
116
+
117
+ end
118
+
119
+ context "with an array of json strings as the body" do
120
+ before do
121
+ @body = [
122
+ JSON.pretty_generate( {'errors' => {'authentication' => ['Unauthorized']}} ),
123
+ JSON.pretty_generate( {'warning' => "warning"} )
124
+ ]
125
+
126
+ allow(@response).to receive(:content_type).and_return('application/json')
127
+ allow(@response).to receive(:body).and_return(@body)
128
+ allow(@inner_app).to receive(:call).and_return([401, {'Content-Type' => 'application/json'}, @body])
129
+ end
130
+
131
+ it 'validates the joined json strings by default' do
132
+ expect(Probity.validators['application/json']).to receive(:call).with(@body.join(""))
133
+ app = described_class.new(@inner_app)
134
+ app.call({})
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,24 @@
1
+ require 'probity'
2
+
3
+ describe "Probity's built-in json validator" do
4
+
5
+ it 'is callable' do
6
+ expect(Probity.validators['application/json']).to respond_to(:call)
7
+ end
8
+
9
+ it 'does not raise on valid json' do
10
+ valid_json = <<-'JSON'
11
+ {"fruit": [
12
+ {"name": "apple",
13
+ "number": 3}
14
+ ]
15
+ }
16
+ JSON
17
+ expect{Probity.validators['application/json'].call(valid_json)}.not_to raise_error
18
+ end
19
+
20
+ it 'raises an error on invalid json' do
21
+ expect{Probity.validators['application/json'].call('{"fruit":[,{"name":"apple","number":3}]}')}.to raise_error
22
+ end
23
+
24
+ end
@@ -0,0 +1,27 @@
1
+ require 'probity'
2
+
3
+ describe "Probity's built-in xml validator" do
4
+
5
+ it 'is callable' do
6
+ expect(Probity.validators['application/xml']).to respond_to(:call)
7
+ end
8
+
9
+ it 'does not raise on valid xml' do
10
+ valid_xml = <<-'XML'
11
+ <?xml version="1.0"?>
12
+ <?xml-stylesheet href="example.xsl" type="text/xsl"?>
13
+ <!DOCTYPE example SYSTEM "example.dtd">
14
+ <fruit>
15
+ <apple color="red" image="red_apple_1.jpg">
16
+ <condition>Fresh</condition>
17
+ </apple>
18
+ </fruit>
19
+ XML
20
+ expect{Probity.validators['application/xml'].call(valid_xml)}.not_to raise_error
21
+ end
22
+
23
+ it 'raises an error on invalid xml' do
24
+ expect{Probity.validators['application/xml'].call('<a></b>')}.to raise_error
25
+ end
26
+
27
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: probity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Weiner
8
+ - Curtis White
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-05-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ~>
19
+ - !ruby/object:Gem::Version
20
+ version: '1.7'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ version: '1.7'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: nokogiri
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ description: Even Rails can't be trusted not to produce malformed xml sometimes. Add
57
+ this Rack middleware to the stack while you run your tests and it will monitor the
58
+ responses you send, infer the appropriate validations based on content type, and
59
+ raise if they fail.
60
+ email:
61
+ - AWeiner@mdsol.com
62
+ - CWhite@mdsol.com
63
+ executables: []
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - lib/probity.rb
71
+ - lib/probity/middleware.rb
72
+ - lib/probity/validators/json.rb
73
+ - lib/probity/validators/xml.rb
74
+ - lib/probity/version.rb
75
+ - probity.gemspec
76
+ - spec/middleware_spec.rb
77
+ - spec/validators/json_spec.rb
78
+ - spec/validators/xml_spec.rb
79
+ homepage: https://github.com/mdsol/probity
80
+ licenses:
81
+ - MIT
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ! '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 2.1.10
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Rack middleware to test whether your app is producing valid json, xml, etc.
103
+ test_files:
104
+ - spec/middleware_spec.rb
105
+ - spec/validators/json_spec.rb
106
+ - spec/validators/xml_spec.rb
107
+ has_rdoc: