diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index bf58efa..0cfcb29 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -50,11 +50,17 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Install sqlite3 + run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 bundler-cache: true + - name: Update RubyGems + run: | + gem update --system 3.2.3 + gem install bundler - name: Run tests run: bundle exec rake - name: Run interaction tests diff --git a/README.md b/README.md index 9803f04..a358e96 100644 --- a/README.md +++ b/README.md @@ -382,16 +382,21 @@ yarn add cypress-on-rails --dev ### for VCR This only works when you start the Rails server with a single worker and single thread +It can be used in two modes: +- with separate insert/eject calls (more general, recommended way) +- with use_cassette wrapper (supports only GraphQL integration) -#### setup +#### basic setup -Add your VCR configuration to your `cypress_helper.rb` +Add your VCR configuration to your `config/cypress_on_rails.rb` ```ruby -require 'vcr' -VCR.configure do |config| - config.hook_into :webmock -end +c.vcr_options = { + hook_into: :webmock, + default_cassette_options: { record: :once }, + # It's possible to override cassette_library_dir using install_folder + cassette_library_dir: File.expand_path("#{__dir__}/../../spec/cypress/fixtures/vcr_cassettes") +} ``` Add to your `cypress/support/index.js`: @@ -408,13 +413,16 @@ VCR.turn_off! WebMock.disable! if defined?(WebMock) ``` +#### insert/eject setup + Add to your `config/cypress_on_rails.rb`: ```ruby c.use_vcr_middleware = !Rails.env.production? && ENV['CYPRESS'].present? + # c.use_vcr_use_cassette_middleware = !Rails.env.production? && ENV['CYPRESS'].present? ``` -#### usage +#### insert/eject usage You have `vcr_insert_cassette` and `vcr_eject_cassette` available. https://www.rubydoc.info/github/vcr/vcr/VCR:insert_cassette @@ -441,6 +449,63 @@ describe('My First Test', () => { }) ``` +#### use_cassette setup + +Add to your `config/cypress_on_rails.rb`: + +```ruby + # c.use_vcr_middleware = !Rails.env.production? && ENV['CYPRESS'].present? + c.use_vcr_use_cassette_middleware = !Rails.env.production? && ENV['CYPRESS'].present? +``` + +Adjust record mode in `config/cypress_on_rails.rb` if needed: + +```ruby +c.vcr_options = { + hook_into: :webmock, + default_cassette_options: { record: :once }, +} +``` + +Add to your `cypress/support/command.js`: + +```js + // Add proxy-like mock to add operation name into query string + Cypress.Commands.add('mockGraphQL', () => { + cy.on('window:before:load', (win) => { + const originalFetch = win.fetch; + const fetch = (path, options, ...rest) => { + if (options && options.body) { + try { + const body = JSON.parse(options.body); + if (body.operationName) { + return originalFetch(`${path}?operation=${body.operationName}`, options, ...rest); + } + } catch (e) { + return originalFetch(path, options, ...rest); + } + } + return originalFetch(path, options, ...rest); + }; + cy.stub(win, 'fetch', fetch); + }); + }); +``` + +Add to your `cypress/support/on-rails.js`, to `beforeEach`: + +```js + cy.mockGraphQL() // for GraphQL usage with use_cassette, see cypress/support/commands.rb +``` + +#### use_cassette usage + +There is nothing special to be called during the Cypress scenario. Each request is wrapped with `VCR.use_cassette`. +Consider VCR configuration in `cypress_helper.rb` to ignore hosts. + +All cassettes will be recorded and saved automatically, using the pattern `/graphql/` + + ## `before_request` configuration You may perform any custom action before running a CypressOnRails command, such as authentication, or sending metrics. Please set `before_request` as part of the CypressOnRails configuration. diff --git a/lib/cypress_on_rails/configuration.rb b/lib/cypress_on_rails/configuration.rb index b7d32e1..9ff0eda 100644 --- a/lib/cypress_on_rails/configuration.rb +++ b/lib/cypress_on_rails/configuration.rb @@ -6,8 +6,10 @@ class Configuration attr_accessor :install_folder attr_accessor :use_middleware attr_accessor :use_vcr_middleware + attr_accessor :use_vcr_use_cassette_middleware attr_accessor :before_request attr_accessor :logger + attr_accessor :vcr_options # Attributes for backwards compatibility def cypress_folder @@ -25,14 +27,17 @@ def initialize alias :use_middleware? :use_middleware alias :use_vcr_middleware? :use_vcr_middleware + alias :use_vcr_use_cassette_middleware? :use_vcr_use_cassette_middleware def reset self.api_prefix = '' self.install_folder = 'spec/e2e' self.use_middleware = true self.use_vcr_middleware = false + self.use_vcr_use_cassette_middleware = false self.before_request = -> (request) {} self.logger = Logger.new(STDOUT) + self.vcr_options = {} end def tagged_logged diff --git a/lib/cypress_on_rails/railtie.rb b/lib/cypress_on_rails/railtie.rb index de6a582..924a81d 100644 --- a/lib/cypress_on_rails/railtie.rb +++ b/lib/cypress_on_rails/railtie.rb @@ -9,8 +9,12 @@ class Railtie < Rails::Railtie app.middleware.use Middleware end if CypressOnRails.configuration.use_vcr_middleware? - require 'cypress_on_rails/vcr_middleware' - app.middleware.use VCRMiddleware + require 'cypress_on_rails/vcr/insert_eject_middleware' + app.middleware.use Vcr::InsertEjectMiddleware + end + if CypressOnRails.configuration.use_vcr_use_cassette_middleware? + require 'cypress_on_rails/vcr/use_cassette_middleware' + app.middleware.use Vcr::UseCassetteMiddleware end end end diff --git a/lib/cypress_on_rails/vcr/insert_eject_middleware.rb b/lib/cypress_on_rails/vcr/insert_eject_middleware.rb new file mode 100644 index 0000000..c67520b --- /dev/null +++ b/lib/cypress_on_rails/vcr/insert_eject_middleware.rb @@ -0,0 +1,75 @@ +require_relative 'middleware_helpers' + +module CypressOnRails + module Vcr + # Middleware to handle vcr with insert/eject endpoints + class InsertEjectMiddleware + include MiddlewareHelpers + + def initialize(app, vcr = nil) + @app = app + @vcr = vcr + @first_call = false + end + + def call(env) + request = Rack::Request.new(env) + if request.path.start_with?('/__e2e__/vcr/insert') + configuration.tagged_logged { handle_insert(request) } + elsif request.path.start_with?('/__e2e__/vcr/eject') + configuration.tagged_logged { handle_eject } + else + do_first_call unless @first_call + @app.call(env) + end + end + + private + + def handle_insert(req) + WebMock.enable! if defined?(WebMock) + vcr.turn_on! + body = parse_request_body(req) + logger.info "vcr insert cassette: #{body}" + cassette_name, options = extract_cassette_info(body) + vcr.insert_cassette(cassette_name, options) + [201, { 'Content-Type' => 'application/json' }, [{ 'message': 'OK' }.to_json]] + rescue JSON::ParserError => e + [400, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]] + rescue LoadError, ArgumentError => e + [500, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]] + end + + def parse_request_body(req) + JSON.parse(req.body.read) + end + + def extract_cassette_info(body) + cassette_name = body[0] + options = (body[1] || {}).symbolize_keys + options[:record] = options[:record].to_sym if options[:record] + options[:match_requests_on] = options[:match_requests_on].map(&:to_sym) if options[:match_requests_on] + options[:serialize_with] = options[:serialize_with].to_sym if options[:serialize_with] + options[:persist_with] = options[:persist_with].to_sym if options[:persist_with] + [cassette_name, options] + end + + def handle_eject + logger.info 'vcr eject cassette' + vcr.eject_cassette + do_first_call + [201, { 'Content-Type' => 'application/json' }, [{ 'message': 'OK' }.to_json]] + rescue LoadError, ArgumentError => e + [500, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]] + end + + def do_first_call + @first_call = true + vcr.turn_off! + WebMock.disable! if defined?(WebMock) + rescue LoadError + # nop + end + end + end +end diff --git a/lib/cypress_on_rails/vcr/middleware_helpers.rb b/lib/cypress_on_rails/vcr/middleware_helpers.rb new file mode 100644 index 0000000..e20d396 --- /dev/null +++ b/lib/cypress_on_rails/vcr/middleware_helpers.rb @@ -0,0 +1,51 @@ +require 'cypress_on_rails/middleware_config' + +module CypressOnRails + module Vcr + # Provides helper methods for VCR middlewares + module MiddlewareHelpers + include MiddlewareConfig + + def vcr + @vcr ||= configure_vcr + end + + def cassette_library_dir + configuration.vcr_options&.fetch(:cassette_library_dir) do + "#{configuration.install_folder}/fixtures/vcr_cassettes" + end + end + + private + + def configure_vcr + require 'vcr' + VCR.configure do |config| + config.cassette_library_dir = cassette_library_dir + apply_vcr_options(config) if configuration.vcr_options.present? + end + VCR + end + + def apply_vcr_options(config) + configuration.vcr_options.each do |option, value| + next if option.to_sym == :cassette_library_dir + + apply_vcr_option(config, option, value) + end + end + + def apply_vcr_option(config, option, value) + return unless config.respond_to?(option) || config.respond_to?("#{option}=") + + if config.respond_to?("#{option}=") + config.send("#{option}=", value) + elsif value.is_a?(Array) + config.send(option, *value) + else + config.send(option, value) + end + end + end + end +end diff --git a/lib/cypress_on_rails/vcr/use_cassette_middleware.rb b/lib/cypress_on_rails/vcr/use_cassette_middleware.rb new file mode 100644 index 0000000..4f492f2 --- /dev/null +++ b/lib/cypress_on_rails/vcr/use_cassette_middleware.rb @@ -0,0 +1,56 @@ +require_relative 'middleware_helpers' + +module CypressOnRails + module Vcr + # Middleware to handle vcr with use_cassette + class UseCassetteMiddleware + include MiddlewareHelpers + + def initialize(app, vcr = nil) + @app = app + @vcr = vcr + end + + def call(env) + return @app.call(env) if should_not_use_vcr? + + initialize_vcr + handle_request_with_vcr(env) + end + + private + + def vcr_defined? + defined?(VCR) != nil + end + + def should_not_use_vcr? + vcr_defined? && + VCR.configuration.cassette_library_dir.present? && + VCR.configuration.cassette_library_dir != cassette_library_dir + end + + def initialize_vcr + WebMock.enable! if defined?(WebMock) + vcr.turn_on! + end + + def handle_request_with_vcr(env) + request = Rack::Request.new(env) + cassette_name = fetch_request_cassette(request) + vcr.use_cassette(cassette_name) do + logger.info "Handle request with cassette name: #{cassette_name}" + @app.call(env) + end + end + + def fetch_request_cassette(request) + if request.path.start_with?('/graphql') && request.params.key?('operation') + "#{request.path}/#{request.params['operation']}" + else + request.path + end + end + end + end +end diff --git a/lib/cypress_on_rails/vcr_middleware.rb b/lib/cypress_on_rails/vcr_middleware.rb deleted file mode 100644 index e675dbc..0000000 --- a/lib/cypress_on_rails/vcr_middleware.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'json' -require 'rack' -require 'cypress_on_rails/middleware_config' - -module CypressOnRails - # Middleware to handle vcr - class VCRMiddleware - include MiddlewareConfig - - def initialize(app, vcr = nil) - @app = app - @vcr = vcr - @first_call = false - end - - def call(env) - request = Rack::Request.new(env) - if request.path.start_with?('/__e2e__/vcr/insert') - configuration.tagged_logged { handle_insert(request) } - elsif request.path.start_with?('/__e2e__/vcr/eject') - configuration.tagged_logged { handle_eject } - else - do_first_call unless @first_call - @app.call(env) - end - end - - private - - def handle_insert(req) - WebMock.enable! if defined?(WebMock) - vcr.turn_on! - body = JSON.parse(req.body.read) - logger.info "vcr insert cassette: #{body}" - cassette_name = body[0] - options = (body[1] || {}).symbolize_keys - options[:record] = options[:record].to_sym if options[:record] - options[:match_requests_on] = options[:match_requests_on].map(&:to_sym) if options[:match_requests_on] - options[:serialize_with] = options[:serialize_with].to_sym if options[:serialize_with] - options[:persist_with] = options[:persist_with].to_sym if options[:persist_with] - vcr.insert_cassette(cassette_name, options) - [201, {'Content-Type' => 'application/json'}, [{'message': 'OK'}.to_json]] - rescue LoadError, ArgumentError => e - [501, {'Content-Type' => 'application/json'}, [{'message': e.message}.to_json]] - end - - def handle_eject - logger.info "vcr eject cassette" - vcr.eject_cassette - do_first_call - [201, {'Content-Type' => 'application/json'}, [{'message': 'OK'}.to_json]] - rescue LoadError, ArgumentError => e - [501, {'Content-Type' => 'application/json'}, [{'message': e.message}.to_json]] - end - - def vcr - return @vcr if @vcr - require 'vcr' - VCR.configure do |config| - config.cassette_library_dir = "#{configuration.install_folder}/fixtures/vcr_cassettes" - end - @vcr = VCR - end - - def do_first_call - @first_call = true - vcr.turn_off! - WebMock.disable! if defined?(WebMock) - rescue LoadError - # nop - end - end -end diff --git a/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb b/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb index 77bd389..4e4212b 100644 --- a/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb +++ b/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb @@ -6,6 +6,14 @@ if defined?(CypressOnRails) # please use with extra caution if enabling on hosted servers or starting your local server on 0.0.0.0 c.use_middleware = !Rails.env.production? <% unless options.experimental %># <% end %> c.use_vcr_middleware = !Rails.env.production? + # Use this if you want to use use_cassette wrapper instead of manual insert/eject + # c.use_vcr_use_cassette_middleware = !Rails.env.production? + # Pass custom VCR options + # c.vcr_options = { + # hook_into: :webmock, + # default_cassette_options: { record: :once }, + # cassette_library_dir: File.expand_path("#{__dir__}/../../<%= options.install_folder %>/<%= options.framework %>/fixtures/vcr_cassettes") + # } c.logger = Rails.logger # If you want to enable a before_request logic, such as authentication, logging, sending metrics, etc. diff --git a/lib/generators/cypress_on_rails/templates/spec/cypress/support/commands.js b/lib/generators/cypress_on_rails/templates/spec/cypress/support/commands.js index c1f5a77..40caa52 100644 --- a/lib/generators/cypress_on_rails/templates/spec/cypress/support/commands.js +++ b/lib/generators/cypress_on_rails/templates/spec/cypress/support/commands.js @@ -23,3 +23,25 @@ // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +// +// +// -- This is for Graphql usage. Add proxy-like mock to add operation name into query string -- +// Cypress.Commands.add('mockGraphQL', () => { +// cy.on('window:before:load', (win) => { +// const originalFetch = win.fetch; +// const fetch = (path, options, ...rest) => { +// if (options && options.body) { +// try { +// const body = JSON.parse(options.body); +// if (body.operationName) { +// return originalFetch(`${path}?operation=${body.operationName}`, options, ...rest); +// } +// } catch (e) { +// return originalFetch(path, options, ...rest); +// } +// } +// return originalFetch(path, options, ...rest); +// }; +// cy.stub(win, 'fetch', fetch); +// }); +// }); diff --git a/lib/generators/cypress_on_rails/templates/spec/cypress/support/on-rails.js b/lib/generators/cypress_on_rails/templates/spec/cypress/support/on-rails.js index 79e8eca..a3bbaef 100644 --- a/lib/generators/cypress_on_rails/templates/spec/cypress/support/on-rails.js +++ b/lib/generators/cypress_on_rails/templates/spec/cypress/support/on-rails.js @@ -47,6 +47,7 @@ Cypress.Commands.add('appFixtures', function (options) { // The next is optional // beforeEach(() => { // cy.app('clean') // have a look at cypress/app_commands/clean.rb +// cy.mockGraphQL() // for GraphQL usage with use_cassette, see cypress/support/commands.rb // }); // comment this out if you do not want to attempt to log additional info on test fail diff --git a/spec/cypress_on_rails/configuration_spec.rb b/spec/cypress_on_rails/configuration_spec.rb index 26678d0..f790abf 100644 --- a/spec/cypress_on_rails/configuration_spec.rb +++ b/spec/cypress_on_rails/configuration_spec.rb @@ -9,22 +9,25 @@ expect(CypressOnRails.configuration.use_middleware?).to eq(true) expect(CypressOnRails.configuration.logger).to_not be_nil expect(CypressOnRails.configuration.before_request).to_not be_nil + expect(CypressOnRails.configuration.vcr_options).to eq({}) end it 'can be configured' do my_logger = Logger.new(STDOUT) - before_request_lambda = -> (_) { return [200, {}, ['hello world']] } + before_request_lambda = ->(_) { return [200, {}, ['hello world']] } CypressOnRails.configure do |config| config.api_prefix = '/api' config.install_folder = 'my/path' config.use_middleware = false config.logger = my_logger config.before_request = before_request_lambda + config.vcr_options = { hook_into: :webmock } end expect(CypressOnRails.configuration.api_prefix).to eq('/api') expect(CypressOnRails.configuration.install_folder).to eq('my/path') expect(CypressOnRails.configuration.use_middleware?).to eq(false) expect(CypressOnRails.configuration.logger).to eq(my_logger) expect(CypressOnRails.configuration.before_request).to eq(before_request_lambda) + expect(CypressOnRails.configuration.vcr_options).to eq(hook_into: :webmock) end end diff --git a/spec/cypress_on_rails/vcr/insert_eject_middleware_spec.rb b/spec/cypress_on_rails/vcr/insert_eject_middleware_spec.rb new file mode 100644 index 0000000..748f306 --- /dev/null +++ b/spec/cypress_on_rails/vcr/insert_eject_middleware_spec.rb @@ -0,0 +1,177 @@ +require 'cypress_on_rails/vcr/insert_eject_middleware' +require 'vcr' +require 'active_support/core_ext/hash' unless {}.respond_to?(:symbolize_keys) + +module CypressOnRails + module Vcr + RSpec.describe InsertEjectMiddleware do + let(:app) { ->(env) { [200, {}, ["app did #{env['PATH_INFO']}"]] } } + let(:vcr) { class_double(VCR, turn_on!: true, turn_off!: true, insert_cassette: true, eject_cassette: true) } + subject { described_class.new(app, vcr) } + + let(:env) { {} } + + let(:response) { subject.call(env) } + + def rack_input(json_value) + StringIO.new(JSON.generate(json_value)) + end + + describe '/__e2e__/vcr/insert' do + before do + env['PATH_INFO'] = '/__e2e__/vcr/insert' + end + + it do + env['rack.input'] = rack_input(['cas1']) + + aggregate_failures do + expect(response).to eq([201, + { 'Content-Type' => 'application/json' }, + ['{"message":"OK"}']]) + expect(vcr).to have_received(:turn_on!) + expect(vcr).to have_received(:insert_cassette).with('cas1', {}) + end + end + + it 'works with record' do + env['rack.input'] = rack_input(['cas1', { 'record' => 'new_episodes' }]) + + aggregate_failures do + expect(response).to eq([201, + { 'Content-Type' => 'application/json' }, + ['{"message":"OK"}']]) + expect(vcr).to have_received(:insert_cassette).with('cas1', record: :new_episodes) + end + end + + it 'works with match_requests_on' do + env['rack.input'] = rack_input(['cas1', { 'match_requests_on' => %w[method uri] }]) + + aggregate_failures do + expect(response).to eq([201, + { 'Content-Type' => 'application/json' }, + ['{"message":"OK"}']]) + expect(vcr).to have_received(:insert_cassette).with('cas1', match_requests_on: %i[method uri]) + end + end + + it 'works with serialize_with' do + env['rack.input'] = rack_input(['cas1', { 'serialize_with' => 'yaml' }]) + + aggregate_failures do + expect(response).to eq([201, + { 'Content-Type' => 'application/json' }, + ['{"message":"OK"}']]) + expect(vcr).to have_received(:insert_cassette).with('cas1', serialize_with: :yaml) + end + end + + it 'works with persist_with' do + env['rack.input'] = rack_input(['cas1', { 'persist_with' => 'file_system' }]) + + aggregate_failures do + expect(response).to eq([201, + { 'Content-Type' => 'application/json' }, + ['{"message":"OK"}']]) + expect(vcr).to have_received(:insert_cassette).with('cas1', persist_with: :file_system) + end + end + + context 'when an error occurs' do + it 'returns a 500 error with the error message' do + env['rack.input'] = rack_input(['cas1']) + allow(vcr).to receive(:insert_cassette).and_raise(ArgumentError.new('Invalid cassette name')) + + expect(response).to eq([ + 500, + { 'Content-Type' => 'application/json' }, + ['{"message":"Invalid cassette name"}'] + ]) + end + + it 'returns a 500 error when LoadError occurs' do + env['rack.input'] = rack_input(['cas1']) + allow(vcr).to receive(:insert_cassette).and_raise(LoadError.new('Cannot load VCR')) + + expect(response).to eq([ + 500, + { 'Content-Type' => 'application/json' }, + ['{"message":"Cannot load VCR"}'] + ]) + end + end + + it 'returns a 400 error when JSON parsing fails' do + env['rack.input'] = StringIO.new('invalid json') + + expect(response).to eq([ + 400, + { 'Content-Type' => 'application/json' }, + ['{"message":"unexpected token at \'invalid json\'"}'] + ]) + end + end + + describe '/__e2e__/vcr/eject' do + before do + env['PATH_INFO'] = '/__e2e__/vcr/eject' + end + + it do + aggregate_failures do + expect(response).to eq([201, + { 'Content-Type' => 'application/json' }, + ['{"message":"OK"}']]) + expect(vcr).to have_received(:turn_off!) + expect(vcr).to have_received(:eject_cassette) + end + end + + context 'when an error occurs' do + it 'returns a 500 error with the error message' do + allow(vcr).to receive(:eject_cassette).and_raise(ArgumentError.new('No cassette to eject')) + + expect(response).to eq([ + 500, + { 'Content-Type' => 'application/json' }, + ['{"message":"No cassette to eject"}'] + ]) + end + + it 'returns a 500 error when LoadError occurs' do + allow(vcr).to receive(:eject_cassette).and_raise(LoadError.new('Cannot load VCR')) + + expect(response).to eq([ + 500, + { 'Content-Type' => 'application/json' }, + ['{"message":"Cannot load VCR"}'] + ]) + end + end + end + + describe '"Other paths"' do + it 'calls vcr turn off the first time' do + env['PATH_INFO'] = '/test' + + expect(response).to eq([200, {}, ['app did /test']]) + expect(vcr).to have_received(:turn_off!) + end + + it 'runs app' do + aggregate_failures do + %w[/ /__e2e__/login command /e2e_command /].each do |path| + env['PATH_INFO'] = path + + response = subject.call(env) + + expect(response).to eq([200, {}, ["app did #{path}"]]) + expect(vcr).to have_received(:turn_off!) + end + end + end + end + end + end +end diff --git a/spec/cypress_on_rails/vcr/use_cassette_middleware_spec.rb b/spec/cypress_on_rails/vcr/use_cassette_middleware_spec.rb new file mode 100644 index 0000000..f41ff93 --- /dev/null +++ b/spec/cypress_on_rails/vcr/use_cassette_middleware_spec.rb @@ -0,0 +1,68 @@ +require 'cypress_on_rails/vcr/use_cassette_middleware' +require 'vcr' +require 'active_support/core_ext/hash' unless {}.respond_to?(:symbolize_keys) + +module CypressOnRails + module Vcr + RSpec.describe UseCassetteMiddleware do + let(:app) { ->(env) { [200, {}, ["app did #{env['PATH_INFO']}"]] } } + let(:vcr) { VCR } + subject { described_class.new(app, vcr) } + + let(:env) { { 'rack.input' => rack_input([]) } } + + let(:response) { subject.call(env) } + + def rack_input(json_value) + StringIO.new(JSON.generate(json_value)) + end + + before do + allow(vcr).to receive(:use_cassette).and_yield + end + + it 'returns the application response using correct graphql cassette' do + env['PATH_INFO'] = '/graphql' + env['QUERY_STRING'] = 'operation=test' + + expect(response).to eq([200, {}, ['app did /graphql']]) + expect(vcr).to have_received(:use_cassette).with('/graphql/test') + end + + it 'returns the application response using default request path cassette' do + allow(CypressOnRails).to receive(:configuration).and_return(double(logger: Logger.new(nil))) + env['PATH_INFO'] = '/test/path' + + expect(response).to eq([200, {}, ['app did /test/path']]) + expect(vcr).to have_received(:use_cassette).with('/test/path') + end + + context 'when VCR cassette library directory does not match' do + before do + allow(VCR.configuration).to receive(:cassette_library_dir).and_return('/different/path') + end + + it 'returns the application response without using VCR' do + env['PATH_INFO'] = '/test/path' + + expect(response).to eq([200, {}, ['app did /test/path']]) + expect(vcr).not_to have_received(:use_cassette) + end + end + + context 'when VCR is not defined' do + before do + allow(subject).to receive(:vcr_defined?).and_return(false) + end + + it 'returns the application response without error' do + env['PATH_INFO'] = '/graphql' + env['QUERY_STRING'] = 'operation=test' + + expect(response).to eq([200, {}, ['app did /graphql']]) + expect(vcr).to have_received(:use_cassette).with('/graphql/test') + end + end + end + end +end diff --git a/spec/cypress_on_rails/vcr_middleware_spec.rb b/spec/cypress_on_rails/vcr_middleware_spec.rb deleted file mode 100644 index fe13616..0000000 --- a/spec/cypress_on_rails/vcr_middleware_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'cypress_on_rails/vcr_middleware' -require 'vcr' -require 'active_support/core_ext/hash' unless Hash.new.respond_to?(:symbolize_keys) - -module CypressOnRails - RSpec.describe VCRMiddleware do - let(:app) { ->(env) { [200, {}, ["app did #{env['PATH_INFO']}"]] } } - let(:vcr) { class_double(VCR, turn_on!: true, turn_off!: true, insert_cassette: true, eject_cassette: true) } - subject { described_class.new(app, vcr) } - - let(:env) { {} } - - let(:response) { subject.call(env) } - - def rack_input(json_value) - StringIO.new(JSON.generate(json_value)) - end - - describe '/__e2e__/vcr/insert' do - before do - env['PATH_INFO'] = '/__e2e__/vcr/insert' - end - - it do - env['rack.input'] = rack_input(['cas1']) - - aggregate_failures do - expect(response).to eq([201, - {"Content-Type"=>"application/json"}, - ["{\"message\":\"OK\"}"]]) - expect(vcr).to have_received(:turn_on!) - expect(vcr).to have_received(:insert_cassette).with('cas1', {}) - end - end - - it 'works with record' do - env['rack.input'] = rack_input(['cas1', { "record" => "new_episodes" }]) - - aggregate_failures do - expect(response).to eq([201, - {"Content-Type"=>"application/json"}, - ["{\"message\":\"OK\"}"]]) - expect(vcr).to have_received(:insert_cassette).with('cas1', record: :new_episodes) - end - end - - it 'works with match_requests_on' do - env['rack.input'] = rack_input(['cas1', { "match_requests_on" => ["method", "uri"] }]) - - aggregate_failures do - expect(response).to eq([201, - {"Content-Type"=>"application/json"}, - ["{\"message\":\"OK\"}"]]) - expect(vcr).to have_received(:insert_cassette).with('cas1', match_requests_on: [:method, :uri]) - end - end - - it 'works with serialize_with' do - env['rack.input'] = rack_input(['cas1', { "serialize_with" => "yaml" }]) - - aggregate_failures do - expect(response).to eq([201, - {"Content-Type"=>"application/json"}, - ["{\"message\":\"OK\"}"]]) - expect(vcr).to have_received(:insert_cassette).with('cas1', serialize_with: :yaml) - end - end - - it 'works with persist_with' do - env['rack.input'] = rack_input(['cas1', { "persist_with" => "file_system" }]) - - aggregate_failures do - expect(response).to eq([201, - {"Content-Type"=>"application/json"}, - ["{\"message\":\"OK\"}"]]) - expect(vcr).to have_received(:insert_cassette).with('cas1', persist_with: :file_system) - end - end - end - - describe '/__e2e__/vcr/eject' do - before do - env['PATH_INFO'] = '/__e2e__/vcr/eject' - end - - it do - aggregate_failures do - expect(response).to eq([201, - {"Content-Type"=>"application/json"}, - ["{\"message\":\"OK\"}"]]) - expect(vcr).to have_received(:turn_off!) - expect(vcr).to have_received(:eject_cassette) - end - end - end - - describe '"Other paths"' do - it 'calls vcr turn off the first time' do - env['PATH_INFO'] = '/test' - - expect(response).to eq([200, {}, ["app did /test"]]) - expect(vcr).to have_received(:turn_off!) - end - - it 'runs app' do - aggregate_failures do - %w(/ /__e2e__/login command /e2e_command /).each do |path| - env['PATH_INFO'] = path - - response = subject.call(env) - - expect(response).to eq([200, {}, ["app did #{path}"]]) - expect(vcr).to have_received(:turn_off!) - end - end - end - end - end -end diff --git a/specs_e2e/rails_3_2/config/boot.rb b/specs_e2e/rails_3_2/config/boot.rb index 4489e58..f2830ae 100644 --- a/specs_e2e/rails_3_2/config/boot.rb +++ b/specs_e2e/rails_3_2/config/boot.rb @@ -3,4 +3,4 @@ # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/specs_e2e/rails_4_2/Gemfile b/specs_e2e/rails_4_2/Gemfile index 835f8b5..3ca9472 100644 --- a/specs_e2e/rails_4_2/Gemfile +++ b/specs_e2e/rails_4_2/Gemfile @@ -3,6 +3,9 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 4.2.10' gem 'sprockets', '~> 3.7.2' +# Solution for issue: NoMethodError: undefined method `new' for BigDecimal:Class +# https://github.com/ruby/bigdecimal?tab=readme-ov-file#which-version-should-you-select +gem 'bigdecimal', '1.3.5' group :development, :test do gem 'vcr'