Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VCR middleware #93

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/cypress_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ class Configuration
attr_accessor :cypress_folder
attr_accessor :use_middleware
attr_accessor :logger
attr_accessor :use_vcr
attr_accessor :vcr_record_mode

def initialize
reset
Expand All @@ -16,6 +18,8 @@ def reset
self.cypress_folder = 'spec/cypress'
self.use_middleware = true
self.logger = Logger.new(STDOUT)
self.use_vcr = false
self.vcr_record_mode = :new_episodes
end

def tagged_logged
Expand Down
5 changes: 4 additions & 1 deletion lib/cypress_on_rails/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'rack'
require 'cypress_on_rails/configuration'
require 'cypress_on_rails/command_executor'
require 'cypress_on_rails/vcr_wrapper'

module CypressOnRails
# Middleware to handle cypress commands and eval
Expand All @@ -16,6 +17,8 @@ def call(env)
request = Rack::Request.new(env)
if request.path.start_with?('/__cypress__/command')
configuration.tagged_logged { handle_command(request) }
elsif defined?(VCR) && configuration.use_vcr
VCRWrapper.new(app: @app, env: env).run_with_cassette
else
@app.call(env)
end
Expand Down Expand Up @@ -53,7 +56,7 @@ def handle_command(req)
body = JSON.parse(req.body.read)
logger.info "handle_command: #{body}"
commands = Command.from_body(body, configuration)
missing_command = commands.find {|command| !@file.exists?(command.file_path) }
missing_command = commands.find {|command| !@file.exist?(command.file_path) }

if missing_command.nil?
begin
Expand Down
38 changes: 38 additions & 0 deletions lib/cypress_on_rails/vcr_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require 'rack'
require 'cypress_on_rails/configuration'

module CypressOnRails
class VCRWrapper

def initialize(app:, env:)
@app = app
@env = env
@request = Rack::Request.new(env)
end

def run_with_cassette
VCR.use_cassette(cassette_name, { :record => configuration.vcr_record_mode }) do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure 🤔 if this will work as it only wraps one request.

I think we actually need two endpoints, one to inject a cassette and one to eject a cassette.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grantspeelman As the doc says https://www.rubydoc.info/gems/vcr/VCR:insert_cassette They recommend to use use_cassette instead of pair of insert-eject calls.
I guess use_cassette block should work the same way. The only change here is wrapping "normal" app flow.
Why do you think that insert/eject will perform differently?
I tested it on the real project under test env, and I've got a cassette written with 2 API calls inside.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guess i just don't see how "use_cassette" approach will work, but maybe once you have a working example it will make sense 👍🏽

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I, it's making sense now.
Every single request it wrapped in it's own VCR cassette 👍🏽 and it's not really controlled from within Cypress.
This a is a good start and could help some teams around certain scenarios.
Maybe all that is still needed from Cypress is the ability to set a cassette prefix so that different scenarios can use different cassettes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grantspeelman I was thinking about some sort of temporary key-value storage, that users can setup in the config and populate values using appCommand during the test. This storage could be reseted the same way as we do it for DB before/after each test. In that perspective, we could pass the whole name for cassette, not just a prefix.
But maybe it's too much.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the whole name for cassette sounds like a good idea, also gets around the issue of having to try and generate a cassette name. Like that idea 👍🏽

logger.info "Handle request with cassette name: #{cassette_name}"
@app.call(@env)
end
end

private

def configuration
CypressOnRails.configuration
end

def logger
configuration.logger
end

def cassette_name
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, the most tricky part is cassette_name.
It should define cassette name based on some data from the request. But to do this right we need to be careful.
I'm not sure we could pass this name from Cypress, b/c BE request could be sent from anywhere (ex. graphql mutation in form submit)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For those projects using GraphQL it would be nice to detect it as well. Right now it considers /graphql as a path for endpoint, but it could be different.
Also, no guarantee, that the operation name will be a part of params

if @request.path.start_with?('/graphql') && @request.params.key?('operation')
"#{@request.path}/#{@request.params['operation']}"
else
@request.path
end
end
end
end
2 changes: 1 addition & 1 deletion lib/generators/cypress_on_rails/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class InstallGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)

def install_cypress
if !Dir.exists?(options.cypress_folder) || Dir["#{options.cypress_folder}/*"].empty?
if !Dir.exist?(options.cypress_folder) || Dir["#{options.cypress_folder}/*"].empty?
directories = options.cypress_folder.split('/')
directories.pop
install_dir = "#{Dir.pwd}/#{directories.join('/')}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ 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?
c.logger = Rails.logger
c.use_vcr = ENV['WITH_VCR'].present?

# # Setup VCR to mock external HTTP requests
# if ENV['WITH_VCR'].present?
# # c.vcr_record_mode = :once # Use to choose VCR record mode (:new_episodes by default)
#
# require 'vcr'
# VCR.configure do |config|
# config.cassette_library_dir = Rails.root.join('spec', 'cypress', 'fixtures', 'cassettes') # Update cassettes path as nedded
# config.hook_into :webmock
# config.ignore_localhost = true
# config.ignore_hosts('localhost', '127.0.0.1', '0.0.0.0')
# end
# end
end

# # if you compile your asssets on CI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
// });
// });
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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, see cypress/support/commands.rb
// });

// comment this out if you do not want to attempt to log additional info on test fail
Expand Down
2 changes: 1 addition & 1 deletion spec/integrations/rails_3_2/config/boot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'])