diff --git a/.rubocop.yml b/.rubocop.yml index 6f4f687..e38fece 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,7 +5,7 @@ AllCops: StyleGuideCopsOnly: true TargetRubyVersion: 2.3 -Metrics/LineLength: +Layout/LineLength: AllowHeredoc: true AllowURI: true URISchemes: diff --git a/.travis.yml b/.travis.yml index 57a0855..a1b0930 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,14 +10,8 @@ script: - bundle exec rubocop rvm: - 2.3.0 - - 2.6.0 - - jruby-9.2.0.0 + - 2.7.0 - ruby-head - - jruby-head -env: - global: - - JRUBY_OPTS='--dev -J-Xmx1024M' matrix: allow_failures: - rvm: ruby-head - - rvm: jruby-head diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de15ee..950de9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.1.0] - To be released + +### Added + +- Stubbing of the HTTP requests using webmock (nepalez) + + ```yaml + --- + - url: example.com/foo + method: get + body: foobar + query: + foo: bar + basic_auth: + user: foo + password: bar + headers: + Accept: utf-8 + responses: + - status: 200 + body: foobar + - status: 404 + ``` + ## [0.0.7] - [2019-07-01] ### Added diff --git a/README.md b/README.md index c84ac68..801e74d 100644 --- a/README.md +++ b/README.md @@ -133,12 +133,25 @@ For message chains: - `arguments` (optional) for specific arguments - `actions` for an array of actions for consecutive invocations of the chain +Every action either `return` some value, or `raise` some exception + For constants: - `const` for stubbed constant - `value` for a value of the constant -Every action either `return` some value, or `raise` some exception +For http requests: + +- `url` or `uri` for the URI of the request (treats values like `/.../` as regular expressions) +- `method` for the specific http-method (like `get` or `post`) +- `body` for the request body (treats values like `/.../` as regular expressions) +- `headers` for the request headers +- `query` for the request query +- `basic_auth` for the `user` and `password` of basic authentication +- `response` or `responses` for consecutively envoked responses with keys: + - `status` + - `body` + - `headers` ```yaml # ./stubs.yml @@ -170,6 +183,21 @@ Every action either `return` some value, or `raise` some exception - const: NOTIFIER_TIMEOUT_SEC value: 10 + +# Examples for stubbing HTTP +- uri: /example.com/foo/ # regexp! + method: delete + basic_auth: + user: foo + password: bar + responses: + - status: 200 # for the first call + - status: 404 # for any other call + +- uri: htpps://example.com/foo # exact string! + method: delete + responses: + - status: 401 ``` ```graphql diff --git a/fixturama.gemspec b/fixturama.gemspec index 9ae5976..6b50727 100644 --- a/fixturama.gemspec +++ b/fixturama.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |gem| gem.name = "fixturama" - gem.version = "0.0.7" + gem.version = "0.1.0" gem.author = "Andrew Kozin (nepalez)" gem.email = "andrew.kozin@gmail.com" gem.homepage = "https://github.com/nepalez/fixturama" @@ -15,9 +15,10 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency "factory_bot", "~> 4.0" gem.add_runtime_dependency "rspec", "~> 3.0" - gem.add_runtime_dependency "hashie", "~> 3.6" + gem.add_runtime_dependency "hashie", "~> 3.0" + gem.add_runtime_dependency "webmock", "~> 3.0" gem.add_development_dependency "rake", "~> 10" - gem.add_development_dependency "rspec-its", "~> 1.2" + gem.add_development_dependency "rspec-its", "~> 1.0" gem.add_development_dependency "rubocop", "~> 0.49" end diff --git a/lib/fixturama.rb b/lib/fixturama.rb index 0720636..59b79e0 100644 --- a/lib/fixturama.rb +++ b/lib/fixturama.rb @@ -3,6 +3,7 @@ require "hashie/mash" require "json" require "rspec" +require "webmock/rspec" require "yaml" module Fixturama diff --git a/lib/fixturama/stubs.rb b/lib/fixturama/stubs.rb index b04dd42..bc3ed32 100644 --- a/lib/fixturama/stubs.rb +++ b/lib/fixturama/stubs.rb @@ -5,6 +5,7 @@ module Fixturama class Stubs require_relative "stubs/chain" require_relative "stubs/const" + require_relative "stubs/request" # # Register new action and apply the corresponding stub @@ -35,14 +36,15 @@ def find_or_create_stub!(options) stub = case stub_type(options) when :message_chain then Chain.new(options) when :constant then Const.new(options) + when :request then Request.new(options) end - @stubs[stub.to_s] ||= stub if stub + @stubs[stub.key] ||= stub if stub end def stub_type(options) - return :message_chain if options[:class] || options[:object] - return :constant if options[:const] + key = (TYPES.keys & options.keys).first + return TYPES[key] if key raise ArgumentError, <<~MESSAGE Cannot figure out what to stub from #{options}. @@ -50,6 +52,26 @@ def stub_type(options) MESSAGE end + # Matches keys to the type of the stub + TYPES = { + arguments: :message_chain, + actions: :message_chain, + basic_auth: :request, + body: :request, + chain: :message_chain, + class: :message_chain, + const: :constant, + headers: :request, + http_method: :request, + object: :message_chain, + query: :request, + response: :request, + responses: :request, + uri: :request, + url: :request, + value: :constant, + }.freeze + def symbolize(options) Hash(options).transform_keys { |key| key.to_s.to_sym } end diff --git a/lib/fixturama/stubs/chain.rb b/lib/fixturama/stubs/chain.rb index 2cbd902..8abebd8 100644 --- a/lib/fixturama/stubs/chain.rb +++ b/lib/fixturama/stubs/chain.rb @@ -16,6 +16,7 @@ def to_s "#{receiver}.#{messages.join(".")}" end alias to_str to_s + alias key to_s # # Register new action for some arguments diff --git a/lib/fixturama/stubs/const.rb b/lib/fixturama/stubs/const.rb index ef586bf..fa90f67 100644 --- a/lib/fixturama/stubs/const.rb +++ b/lib/fixturama/stubs/const.rb @@ -13,6 +13,7 @@ def to_s const.to_s end alias to_str to_s + alias key to_s # # Overload the definition for the constant diff --git a/lib/fixturama/stubs/request.rb b/lib/fixturama/stubs/request.rb new file mode 100644 index 0000000..416ee69 --- /dev/null +++ b/lib/fixturama/stubs/request.rb @@ -0,0 +1,98 @@ +# +# Stubbed request +# +class Fixturama::Stubs::Request + require_relative "request/response" + require_relative "request/responses" + + def to_s + "#{http_method.upcase} #{uri.to_s == "" ? "*" : uri}" + end + alias to_str to_s + + # every stub is unique + alias key hash + def update!(_); end + + def apply!(example) + stub = example.stub_request(http_method, uri) + stub = stub.with(request) if request.any? + stub.to_return { |_| responses.next } + end + + private + + attr_reader :options + + def initialize(options) + @options = options + with_error { @options = Hash(options).symbolize_keys } + end + + HTTP_METHODS = %i[get post put patch delete head options any].freeze + + def http_method + value = with_error("method") { options[:method]&.to_sym&.downcase } || :any + return value if HTTP_METHODS.include?(value) + + raise ArgumentError, "Invalid HTTP method #{value} in #{@optons}" + end + + def uri + with_error("uri") { maybe_regexp(options[:uri] || options[:url]) } + end + + def headers + with_error("headers") do + Hash(options[:headers]).transform_keys(&:to_s) if options.key?(:headers) + end + end + + def query + with_error("query") do + Hash(options[:query]).transform_keys(&:to_s) if options.key?(:query) + end + end + + def body + with_error("body") do + case options[:body] + when NilClass then nil + when Hash then options[:body] + else maybe_regexp(options[:body]) + end + end + end + + def basic_auth + with_error("basic auth") do + value = options[:auth] || options[:basic_auth] + Hash(value).transform_keys(&:to_s).values_at("user", "pass") if value + end + end + + def request + @request ||= { + headers: headers, + body: body, + query: query, + basic_auth: basic_auth + }.select { |_, val| val } + end + + def responses + @responses ||= Responses.new(options[:response] || options[:responses]) + end + + def with_error(part = nil) + yield + rescue RuntimeError + message = ["Cannot extract a request", part, "from #{options}"].join(" ") + raise ArgumentError, message, __FILE__, __LINE__ - 1 + end + + def maybe_regexp(str) + str = str.to_s + str[%r{\A/.*/\z}] ? Regexp.new(str[1..-2]) : str + end +end diff --git a/lib/fixturama/stubs/request/response.rb b/lib/fixturama/stubs/request/response.rb new file mode 100644 index 0000000..fb3c0b4 --- /dev/null +++ b/lib/fixturama/stubs/request/response.rb @@ -0,0 +1,43 @@ +class Fixturama::Stubs::Request + class Response + def to_h + { status: status, body: body, headers: headers }.select { |_, val| val } + end + + private + + def initialize(options) + @options = options + @options = with_error { Hash(options).transform_keys(&:to_sym) } + end + + attr_reader :options + + def status + with_error("status") { options[:status]&.to_i } || 200 + end + + def body + with_error("body") do + case options[:body] + when NilClass then nil + when Hash then JSON.dump(options[:body]) + else options[:body].to_s + end + end + end + + def headers + with_error("headers") do + Hash(options[:headers]).map { |k, v| [k.to_s, v.to_s] }.to_h + end + end + + def with_error(part = nil) + yield + rescue RuntimeError + text = ["Cannot extract a response", part, "from #{options}"].join(" ") + raise ArgumentError, text, __FILE__, __LINE__ - 1 + end + end +end diff --git a/lib/fixturama/stubs/request/responses.rb b/lib/fixturama/stubs/request/responses.rb new file mode 100644 index 0000000..fe1a2b7 --- /dev/null +++ b/lib/fixturama/stubs/request/responses.rb @@ -0,0 +1,20 @@ +class Fixturama::Stubs::Request + class Responses + def next + list.count > @count ? list[@count].tap { @count += 1 } : list.last + end + + private + + def initialize(list) + @count = 0 + list ||= [{ status: 200 }] + @list = case list + when Array then list.map { |item| Response.new(item).to_h } + else [Response.new(list).to_h] + end + end + + attr_reader :list + end +end diff --git a/spec/fixturama/seed_fixture/_spec.rb b/spec/fixturama/seed_fixture/_spec.rb index 458854a..e9c0fd2 100644 --- a/spec/fixturama/seed_fixture/_spec.rb +++ b/spec/fixturama/seed_fixture/_spec.rb @@ -26,7 +26,10 @@ it "runs the factory", aggregate_failures: true do expect(FactoryBot).to receive(:create_list).with(:foo, 1, :baz, qux: 42) - expect(FactoryBot).to receive(:create_list).with(:foo, 3, :bar, {}) + expect(FactoryBot).to receive(:create_list) do |*args, **opts| + expect(args).to eq [:foo, 3, :bar] + expect(opts).to be_empty + end subject end diff --git a/spec/fixturama/stub_fixture/_spec.rb b/spec/fixturama/stub_fixture/_spec.rb index ee917db..1f94a4b 100644 --- a/spec/fixturama/stub_fixture/_spec.rb +++ b/spec/fixturama/stub_fixture/_spec.rb @@ -93,4 +93,28 @@ def pay(_) expect(TIMEOUT).to eq 10 end end + + context "when http request stubbed" do + before { stub_fixture "#{__dir__}/stub.yml" } + + it "stubs the request properly" do + req = Net::HTTP::Get.new("/foo") + res = Net::HTTP.start("www.example.com") { |http| http.request(req) } + + expect(res.code).to eq "200" + expect(res.body).to eq "foo" + expect(res["Content-Length"]).to eq "3" + end + + def delete_request + req = Net::HTTP::Delete.new("/foo") + Net::HTTP.start("www.example.com") { |http| http.request(req) } + end + + it "stubs repetitive requests properly" do + expect(delete_request.code).to eq "200" + expect(delete_request.code).to eq "404" + expect(delete_request.code).to eq "404" + end + end end diff --git a/spec/fixturama/stub_fixture/stub.yml b/spec/fixturama/stub_fixture/stub.yml index 979dd71..1e721a5 100644 --- a/spec/fixturama/stub_fixture/stub.yml +++ b/spec/fixturama/stub_fixture/stub.yml @@ -58,3 +58,16 @@ - const: TIMEOUT value: 10 + +- method: get + uri: www.example.com/foo + responses: + - body: foo + headers: + Content-Length: 3 + +- method: delete + uri: /example.com/foo/ # Regexp! + responses: + - status: 200 + - status: 404 # for any request except for the first one