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 use_cassette middleware #167

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions lib/cypress_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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_use_cassette_mode

# Attributes for backwards compatibility
def cypress_folder
Expand All @@ -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_use_cassette_mode = :new_episodes
end

def tagged_logged
Expand Down
8 changes: 6 additions & 2 deletions lib/cypress_on_rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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_inject_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
Expand Down
39 changes: 39 additions & 0 deletions lib/cypress_on_rails/vcr/base_middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require 'json'
require 'rack'
require 'cypress_on_rails/middleware_config'

module CypressOnRails
module Vcr
# Base abstract Middleware
class BaseMiddleware
include MiddlewareConfig

def initialize(_app, _vcr = nil)
raise_not_implemented
end

def call(_env)
raise_not_implemented
end

def vcr
@vcr ||= configure_vcr
end
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved

private

def configure_vcr
require 'vcr'
VCR.configure do |config|
config.cassette_library_dir = "#{configuration.install_folder}/fixtures/vcr_cassettes"
end
VCR
end

def raise_not_implemented
raise NotImplementedError,
'BaseMiddleware can not be initialized directly, use InsertEjectMiddleware or UseCassetteMiddleware'
end
end
end
end
71 changes: 71 additions & 0 deletions lib/cypress_on_rails/vcr/insert_eject_middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require_relative 'base_middleware'

module CypressOnRails
module Vcr
# Middleware to handle vcr with insert/eject endpoints
class InsertEjectMiddleware < BaseMiddleware
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 }
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved
else
do_first_call unless @first_call
@app.call(env)
end
end
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved

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 LoadError, ArgumentError => e
[501, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]]
end
Copy link

@coderabbitai coderabbitai bot Sep 27, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use appropriate HTTP status code for server errors

In the rescue clauses of handle_insert and handle_eject, 501 Not Implemented is used to indicate errors caused by LoadError or ArgumentError. The 501 status code implies that the server does not support the functionality required to fulfill the request, which may not accurately represent these error cases. Consider using 500 Internal Server Error to represent unexpected server errors more appropriately.

Apply this diff to correct the status code:

-rescue LoadError, ArgumentError => e
-  [501, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]]
+rescue LoadError, ArgumentError => e
+  [500, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]]

Also applies to: 59-61

Copy link
Collaborator

Choose a reason for hiding this comment

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

agree 👍🏽

Copy link

Choose a reason for hiding this comment

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

@grantspeelman Glad you agree!

(^・ω・^)


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
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved

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]]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
rescue LoadError, ArgumentError => e
[501, { '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 do_first_call
@first_call = true
vcr.turn_off!
WebMock.disable! if defined?(WebMock)
rescue LoadError
# nop
end
end
end
end
41 changes: 41 additions & 0 deletions lib/cypress_on_rails/vcr/use_cassette_middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'cypress_on_rails/configuration'
require_relative 'base_middleware'

module CypressOnRails
module Vcr
# Middleware to handle vcr with use_cassette
class UseCassetteMiddleware < BaseMiddleware
def initialize(app, vcr = nil)
@app = app
@vcr = vcr
end
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved

def call(env)
request = Rack::Request.new(env)
cassette_name = fetch_request_cassette(request)
vcr.use_cassette(cassette_name, { record: configuration.vcr_use_cassette_mode }) do
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 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
73 changes: 0 additions & 73 deletions lib/cypress_on_rails/vcr_middleware.rb

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ 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?
# c.vcr_use_cassette_mode = :once # Use to choose VCR record mode (:new_episodes by default)
c.logger = Rails.logger

# If you want to enable a before_request logic, such as authentication, logging, sending metrics, etc.
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 @@ -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, see cypress/support/commands.rb
// });

// comment this out if you do not want to attempt to log additional info on test fail
Expand Down
Loading
Loading