diff --git a/airborne.gemspec b/airborne.gemspec index e8b5a42..2deb655 100644 --- a/airborne.gemspec +++ b/airborne.gemspec @@ -12,5 +12,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rest-client', '~> 1.7', '>= 1.7.3' # version 1.7.3 fixes security vulnerability https://github.com/brooklynDev/airborne/issues/41 s.add_runtime_dependency 'rack-test', '~> 0.6', '>= 0.6.2' s.add_runtime_dependency 'activesupport', '>= 3.0.0', '>= 3.0.0' + s.add_runtime_dependency 'json-schema' + s.add_runtime_dependency 'json-schema-generator' s.add_development_dependency 'webmock', '~> 0' end diff --git a/lib/airborne.rb b/lib/airborne.rb index 719b443..adcc1af 100644 --- a/lib/airborne.rb +++ b/lib/airborne.rb @@ -16,6 +16,10 @@ config.add_setting :rack_app config.add_setting :requester_type config.add_setting :requester_module + config.add_setting :generate_schema, default: false + config.add_setting :schema_path, default: 'schema' + config.add_setting :validate_schema, default: false + config.add_setting :schema_version, default: 'draft4' config.before do |example| config.match_expected = example.metadata[:match_expected].nil? ? Airborne.configuration.match_expected_default? : example.metadata[:match_expected] diff --git a/lib/airborne/base.rb b/lib/airborne/base.rb index 7f900eb..47fa626 100644 --- a/lib/airborne/base.rb +++ b/lib/airborne/base.rb @@ -7,7 +7,7 @@ class InvalidJsonError < StandardError; end include RequestExpectations - attr_reader :response, :headers, :body + attr_reader :response, :headers, :body, :rest_url, :rest_method def self.configure RSpec.configure do |config| @@ -30,22 +30,27 @@ def self.configuration end def get(url, headers = nil) + @rest_method = :get @response = make_request(:get, url, headers: headers) end def post(url, post_body = nil, headers = nil) + @rest_method = :post @response = make_request(:post, url, body: post_body, headers: headers) end def patch(url, patch_body = nil, headers = nil) + @rest_method = :patch @response = make_request(:patch, url, body: patch_body, headers: headers) end def put(url, put_body = nil, headers = nil) + @rest_method = :put @response = make_request(:put, url, body: put_body, headers: headers) end def delete(url, delete_body = nil, headers = nil) + @rest_method = :delete @response = make_request(:delete, url, body: delete_body, headers: headers) end @@ -76,6 +81,7 @@ def json_body private def get_url(url) + @rest_url = url base = Airborne.configuration.base_url || '' base + url end diff --git a/lib/airborne/request_expectations.rb b/lib/airborne/request_expectations.rb index 3cd3297..e8db50f 100644 --- a/lib/airborne/request_expectations.rb +++ b/lib/airborne/request_expectations.rb @@ -1,6 +1,7 @@ require 'rspec' require 'date' require 'rack/utils' +require 'json-schema' module Airborne class ExpectationError < StandardError; end @@ -44,6 +45,28 @@ def expect_header_contains(key, content) expect_header_impl(key, content, true) end + def expect_json_schema(file=nil) + path = "#{Dir.pwd}/#{Airborne.configuration.schema_path}#{rest_url}" + path = path.tr('?', '/').tr('&', '/').tr('[', '-').tr(']', '') + path = path.gsub(/\/\d*\//, "/ID/").gsub(/\/\d*$/, "/ID").tr('=', '/') + + file ||= "#{path}/#{rest_method}.schema" + + schema_details = JSON.parse File.read(file) + schema = if schema_details['items'] + schema_details['items'] + else + schema_details['properties'] + end + + expect { JSON::Validator.validate!(schema, json_body) }.not_to raise_error + rescue Errno::ENOENT => ex + warn "Can not verify schema; #{ex.message}" + ensure + json_file = file.gsub('.schema', '.json') + File.delete json_file if File.exist? json_file + end + def optional(hash) OptionalHashTypeExpectations.new(hash) end diff --git a/lib/airborne/rest_client_requester.rb b/lib/airborne/rest_client_requester.rb index 3ebe68f..479b9c2 100644 --- a/lib/airborne/rest_client_requester.rb +++ b/lib/airborne/rest_client_requester.rb @@ -1,22 +1,25 @@ require 'rest_client' +require 'json-schema-generator' module Airborne module RestClientRequester def make_request(method, url, options = {}) headers = base_headers.merge(options[:headers] || {}) - res = if method == :post || method == :patch || method == :put + if method == :post || method == :patch || method == :put begin request_body = options[:body].nil? ? '' : options[:body] request_body = request_body.to_json if options[:body].is_a?(Hash) - RestClient.send(method, get_url(url), request_body, headers) + res = RestClient.send(method, get_url(url), request_body, headers) + generate_json(method, url, res) if Airborne.configuration.validate_schema rescue RestClient::Exception => e - e.response + res = e.response end else begin - RestClient.send(method, get_url(url), headers) + res = RestClient.send(method, get_url(url), headers) + generate_json(method, url, res) if Airborne.configuration.validate_schema rescue RestClient::Exception => e - e.response + res = e.response end end res @@ -27,5 +30,26 @@ def make_request(method, url, options = {}) def base_headers { content_type: :json }.merge(Airborne.configuration.headers || {}) end + + def generate_json(method, url, res) + path = "#{Dir.pwd}/#{Airborne.configuration.schema_path}#{url}/" + path = path.tr('?', '/').tr('&', '/').tr('[', '-').tr(']', '') + path = path.gsub(/\/\d*\//, "/ID/").gsub(/\/\d*$/, "/ID").tr('=', '/') + + file = "#{path}/#{method}.json" + + unless File.exist?(file.gsub('.json', '.schema')) + return unless Airborne.configuration.generate_schema # no schema to compare against + FileUtils.mkdir_p(path) + end + + File.open(file, 'w') { |f| f.puts res } + + schema_file = file.gsub(".json", ".schema") + return unless File.exist?(schema_file) || Airborne.configuration.generate_schema + + schema = JSON::SchemaGenerator.generate file, File.read(file), {:schema_version => Airborne.configuration.schema_version} + File.open(schema_file, 'w') { |f| f.puts schema } + end end end