Skip to content

Commit f2bcd5b

Browse files
committed
Add VCR use_cassette support
1 parent c89651e commit f2bcd5b

File tree

14 files changed

+545
-202
lines changed

14 files changed

+545
-202
lines changed

README.md

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -382,16 +382,21 @@ yarn add cypress-on-rails --dev
382382
### for VCR
383383

384384
This only works when you start the Rails server with a single worker and single thread
385+
It can be used in two modes:
386+
- with separate insert/eject calls (more general, recommended way)
387+
- with use_cassette wrapper (supports only GraphQL integration)
385388

386-
#### setup
389+
#### basic setup
387390

388-
Add your VCR configuration to your `cypress_helper.rb`
391+
Add your VCR configuration to your `config/cypress_on_rails.rb`
389392

390393
```ruby
391-
require 'vcr'
392-
VCR.configure do |config|
393-
config.hook_into :webmock
394-
end
394+
c.vcr_options = {
395+
hook_into: :webmock,
396+
default_cassette_options: { record: :once },
397+
# It's possible to override cassette_library_dir using install_folder
398+
cassette_library_dir: File.expand_path("#{__dir__}/../../spec/cypress/fixtures/vcr_cassettes")
399+
}
395400
```
396401

397402
Add to your `cypress/support/index.js`:
@@ -408,13 +413,16 @@ VCR.turn_off!
408413
WebMock.disable! if defined?(WebMock)
409414
```
410415

416+
#### insert/eject setup
417+
411418
Add to your `config/cypress_on_rails.rb`:
412419

413420
```ruby
414421
c.use_vcr_middleware = !Rails.env.production? && ENV['CYPRESS'].present?
422+
# c.use_vcr_use_cassette_middleware = !Rails.env.production? && ENV['CYPRESS'].present?
415423
```
416424

417-
#### usage
425+
#### insert/eject usage
418426

419427
You have `vcr_insert_cassette` and `vcr_eject_cassette` available. https://www.rubydoc.info/github/vcr/vcr/VCR:insert_cassette
420428

@@ -441,6 +449,63 @@ describe('My First Test', () => {
441449
})
442450
```
443451

452+
#### use_cassette setup
453+
454+
Add to your `config/cypress_on_rails.rb`:
455+
456+
```ruby
457+
# c.use_vcr_middleware = !Rails.env.production? && ENV['CYPRESS'].present?
458+
c.use_vcr_use_cassette_middleware = !Rails.env.production? && ENV['CYPRESS'].present?
459+
```
460+
461+
Adjust record mode in `config/cypress_on_rails.rb` if needed:
462+
463+
```ruby
464+
c.vcr_options = {
465+
hook_into: :webmock,
466+
default_cassette_options: { record: :once },
467+
}
468+
```
469+
470+
Add to your `cypress/support/command.js`:
471+
472+
```js
473+
// Add proxy-like mock to add operation name into query string
474+
Cypress.Commands.add('mockGraphQL', () => {
475+
cy.on('window:before:load', (win) => {
476+
const originalFetch = win.fetch;
477+
const fetch = (path, options, ...rest) => {
478+
if (options && options.body) {
479+
try {
480+
const body = JSON.parse(options.body);
481+
if (body.operationName) {
482+
return originalFetch(`${path}?operation=${body.operationName}`, options, ...rest);
483+
}
484+
} catch (e) {
485+
return originalFetch(path, options, ...rest);
486+
}
487+
}
488+
return originalFetch(path, options, ...rest);
489+
};
490+
cy.stub(win, 'fetch', fetch);
491+
});
492+
});
493+
```
494+
495+
Add to your `cypress/support/on-rails.js`, to `beforeEach`:
496+
497+
```js
498+
cy.mockGraphQL() // for GraphQL usage with use_cassette, see cypress/support/commands.rb
499+
```
500+
501+
#### use_cassette usage
502+
503+
There is nothing special to be called during the Cypress scenario. Each request is wrapped with `VCR.use_cassette`.
504+
Consider VCR configuration in `cypress_helper.rb` to ignore hosts.
505+
506+
All cassettes will be recorded and saved automatically, using the pattern `<vcs_cassettes_path>/graphql/<operation_name>`
507+
508+
444509
## `before_request` configuration
445510

446511
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.

lib/cypress_on_rails/configuration.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ class Configuration
66
attr_accessor :install_folder
77
attr_accessor :use_middleware
88
attr_accessor :use_vcr_middleware
9+
attr_accessor :use_vcr_use_cassette_middleware
910
attr_accessor :before_request
1011
attr_accessor :logger
12+
attr_accessor :vcr_options
1113

1214
# Attributes for backwards compatibility
1315
def cypress_folder
@@ -25,14 +27,17 @@ def initialize
2527

2628
alias :use_middleware? :use_middleware
2729
alias :use_vcr_middleware? :use_vcr_middleware
30+
alias :use_vcr_use_cassette_middleware? :use_vcr_use_cassette_middleware
2831

2932
def reset
3033
self.api_prefix = ''
3134
self.install_folder = 'spec/e2e'
3235
self.use_middleware = true
3336
self.use_vcr_middleware = false
37+
self.use_vcr_use_cassette_middleware = false
3438
self.before_request = -> (request) {}
3539
self.logger = Logger.new(STDOUT)
40+
self.vcr_options = {}
3641
end
3742

3843
def tagged_logged

lib/cypress_on_rails/railtie.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ class Railtie < Rails::Railtie
99
app.middleware.use Middleware
1010
end
1111
if CypressOnRails.configuration.use_vcr_middleware?
12-
require 'cypress_on_rails/vcr_middleware'
13-
app.middleware.use VCRMiddleware
12+
require 'cypress_on_rails/vcr/insert_eject_middleware'
13+
app.middleware.use Vcr::InsertEjectMiddleware
14+
end
15+
if CypressOnRails.configuration.use_vcr_use_cassette_middleware?
16+
require 'cypress_on_rails/vcr/use_cassette_middleware'
17+
app.middleware.use Vcr::UseCassetteMiddleware
1418
end
1519
end
1620
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
require_relative 'middleware_helpers'
2+
3+
module CypressOnRails
4+
module Vcr
5+
# Middleware to handle vcr with insert/eject endpoints
6+
class InsertEjectMiddleware
7+
include MiddlewareHelpers
8+
9+
def initialize(app, vcr = nil)
10+
@app = app
11+
@vcr = vcr
12+
@first_call = false
13+
end
14+
15+
def call(env)
16+
request = Rack::Request.new(env)
17+
if request.path.start_with?('/__e2e__/vcr/insert')
18+
configuration.tagged_logged { handle_insert(request) }
19+
elsif request.path.start_with?('/__e2e__/vcr/eject')
20+
configuration.tagged_logged { handle_eject }
21+
else
22+
do_first_call unless @first_call
23+
@app.call(env)
24+
end
25+
end
26+
27+
private
28+
29+
def handle_insert(req)
30+
WebMock.enable! if defined?(WebMock)
31+
vcr.turn_on!
32+
body = parse_request_body(req)
33+
logger.info "vcr insert cassette: #{body}"
34+
cassette_name, options = extract_cassette_info(body)
35+
vcr.insert_cassette(cassette_name, options)
36+
[201, { 'Content-Type' => 'application/json' }, [{ 'message': 'OK' }.to_json]]
37+
rescue JSON::ParserError => e
38+
[400, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]]
39+
rescue LoadError, ArgumentError => e
40+
[500, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]]
41+
end
42+
43+
def parse_request_body(req)
44+
JSON.parse(req.body.read)
45+
end
46+
47+
def extract_cassette_info(body)
48+
cassette_name = body[0]
49+
options = (body[1] || {}).symbolize_keys
50+
options[:record] = options[:record].to_sym if options[:record]
51+
options[:match_requests_on] = options[:match_requests_on].map(&:to_sym) if options[:match_requests_on]
52+
options[:serialize_with] = options[:serialize_with].to_sym if options[:serialize_with]
53+
options[:persist_with] = options[:persist_with].to_sym if options[:persist_with]
54+
[cassette_name, options]
55+
end
56+
57+
def handle_eject
58+
logger.info 'vcr eject cassette'
59+
vcr.eject_cassette
60+
do_first_call
61+
[201, { 'Content-Type' => 'application/json' }, [{ 'message': 'OK' }.to_json]]
62+
rescue LoadError, ArgumentError => e
63+
[500, { 'Content-Type' => 'application/json' }, [{ 'message': e.message }.to_json]]
64+
end
65+
66+
def do_first_call
67+
@first_call = true
68+
vcr.turn_off!
69+
WebMock.disable! if defined?(WebMock)
70+
rescue LoadError
71+
# nop
72+
end
73+
end
74+
end
75+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require 'cypress_on_rails/middleware_config'
2+
3+
module CypressOnRails
4+
module Vcr
5+
# Provides helper methods for VCR middlewares
6+
module MiddlewareHelpers
7+
include MiddlewareConfig
8+
9+
def vcr
10+
@vcr ||= configure_vcr
11+
end
12+
13+
def cassette_library_dir
14+
configuration.vcr_options&.fetch(:cassette_library_dir) do
15+
"#{configuration.install_folder}/fixtures/vcr_cassettes"
16+
end
17+
end
18+
19+
private
20+
21+
def configure_vcr
22+
require 'vcr'
23+
VCR.configure do |config|
24+
config.cassette_library_dir = cassette_library_dir
25+
apply_vcr_options(config) if configuration.vcr_options.present?
26+
end
27+
VCR
28+
end
29+
30+
def apply_vcr_options(config)
31+
configuration.vcr_options.each do |option, value|
32+
next if option.to_sym == :cassette_library_dir
33+
34+
apply_vcr_option(config, option, value)
35+
end
36+
end
37+
38+
def apply_vcr_option(config, option, value)
39+
return unless config.respond_to?(option) || config.respond_to?("#{option}=")
40+
41+
if config.respond_to?("#{option}=")
42+
config.send("#{option}=", value)
43+
elsif value.is_a?(Array)
44+
config.send(option, *value)
45+
else
46+
config.send(option, value)
47+
end
48+
end
49+
end
50+
end
51+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
require_relative 'middleware_helpers'
2+
3+
module CypressOnRails
4+
module Vcr
5+
# Middleware to handle vcr with use_cassette
6+
class UseCassetteMiddleware
7+
include MiddlewareHelpers
8+
9+
def initialize(app, vcr = nil)
10+
@app = app
11+
@vcr = vcr
12+
end
13+
14+
def call(env)
15+
return @app.call(env) if should_not_use_vcr?
16+
17+
initialize_vcr
18+
handle_request_with_vcr(env)
19+
end
20+
21+
private
22+
23+
def vcr_defined?
24+
defined?(VCR) != nil
25+
end
26+
27+
def should_not_use_vcr?
28+
vcr_defined? &&
29+
VCR.configuration.cassette_library_dir.present? &&
30+
VCR.configuration.cassette_library_dir != cassette_library_dir
31+
end
32+
33+
def initialize_vcr
34+
WebMock.enable! if defined?(WebMock)
35+
vcr.turn_on!
36+
end
37+
38+
def handle_request_with_vcr(env)
39+
request = Rack::Request.new(env)
40+
cassette_name = fetch_request_cassette(request)
41+
vcr.use_cassette(cassette_name) do
42+
logger.info "Handle request with cassette name: #{cassette_name}"
43+
@app.call(env)
44+
end
45+
end
46+
47+
def fetch_request_cassette(request)
48+
if request.path.start_with?('/graphql') && request.params.key?('operation')
49+
"#{request.path}/#{request.params['operation']}"
50+
else
51+
request.path
52+
end
53+
end
54+
end
55+
end
56+
end

0 commit comments

Comments
 (0)