Skip to content

Commit

Permalink
Merge 9213daf into dcea719
Browse files Browse the repository at this point in the history
  • Loading branch information
MUTOgen authored Dec 29, 2024
2 parents dcea719 + 9213daf commit 0546e4f
Show file tree
Hide file tree
Showing 17 changed files with 555 additions and 203 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 72 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand All @@ -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

Expand All @@ -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 `<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_options

# 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_options = {}
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
75 changes: 75 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,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
51 changes: 51 additions & 0 deletions lib/cypress_on_rails/vcr/middleware_helpers.rb
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 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,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
Loading

0 comments on commit 0546e4f

Please sign in to comment.