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 17 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
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,11 @@ 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`

Expand All @@ -381,13 +384,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

Expand All @@ -414,6 +420,60 @@ 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_record_mode = :once # Use to choose VCR record mode
```

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 `<vcs_cassettes_path>/graphql/<operation_name>`


## `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.
Expand Down
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_record_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_record_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_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
Expand Down
72 changes: 72 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,72 @@
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)
super(app, vcr)
@first_call = false
end
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved

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
33 changes: 33 additions & 0 deletions lib/cypress_on_rails/vcr/middleware_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'cypress_on_rails/middleware_config'

module CypressOnRails
module Vcr
# Provides helper methods for VCR middlewares
module MiddlewareHelpers
include MiddlewareConfig

def initialize(app, vcr = nil)
@app = app
@vcr = vcr
end

def vcr
@vcr ||= configure_vcr
end

def cassette_library_dir
"#{configuration.install_folder}/fixtures/vcr_cassettes"
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 = cassette_library_dir
end
VCR
end
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
51 changes: 51 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,51 @@
require_relative 'middleware_helpers'

module CypressOnRails
module Vcr
# Middleware to handle vcr with use_cassette
class UseCassetteMiddleware
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved
include MiddlewareHelpers

def call(env)
return @app.call(env) if should_not_use_vcr?
Copy link
Author

Choose a reason for hiding this comment

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

This is a workaround to skip behavior if VCR is already in use (rspec, for example)


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? &&
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved
VCR.configuration.cassette_library_dir != cassette_library_dir
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved
end

def initialize_vcr
WebMock.enable! if defined?(WebMock)
vcr.turn_on!
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved
end

def handle_request_with_vcr(env)
request = Rack::Request.new(env)
cassette_name = fetch_request_cassette(request)
vcr.use_cassette(cassette_name, { record: configuration.vcr_record_mode }) do
logger.info "Handle request with cassette name: #{cassette_name}"
MUTOgen marked this conversation as resolved.
Show resolved Hide resolved
@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
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_record_mode = :once # Use to choose VCR record mode
c.logger = Rails.logger

# If you want to enable a before_request logic, such as authentication, logging, sending metrics, etc.
Expand Down
Loading
Loading