Test augur with puffing-billy to mock browser requests with a proxy

pull/7995/head
Oliver Günther 5 years ago
parent 4d74b25141
commit 5f422e46d9
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 5
      Gemfile
  2. 24
      Gemfile.lock
  3. 29
      frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.html
  4. 217
      spec/features/admin/enterprise/enterprise_trial_spec.rb
  5. 1
      spec/rails_helper.rb
  6. 33
      spec/support/browsers/chrome.rb
  7. 23
      spec/support/browsers/firefox.rb
  8. 39
      spec/support/puffing_billy_proxy.rb
  9. 12
      spec/support/webmock.rb

@ -221,8 +221,13 @@ group :test do
gem 'fuubar', '~> 2.5.0'
gem 'timecop', '~> 0.9.0'
# Mock backend requests (for ruby tests)
gem 'webmock', '~> 3.8.2', require: false
# Mock selenium requests through proxy (for feature tests)
gem 'puffing-billy', '~> 2.3.1'
gem 'equivalent-xml', '~> 0.6'
gem 'json_spec', '~> 1.1.4'
gem 'shoulda-matchers', '~> 3.1', require: nil

@ -358,6 +358,7 @@ GEM
compare-xml (0.66)
nokogiri (~> 1.8)
concurrent-ruby (1.1.6)
cookiejar (0.3.3)
cork (0.3.0)
colored2 (~> 3.1)
crack (0.4.3)
@ -458,6 +459,16 @@ GEM
dry-equalizer (~> 0.3)
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 1.0, >= 1.0.2)
em-http-request (1.1.5)
addressable (>= 2.3.4)
cookiejar (!= 0.3.1)
em-socksify (>= 0.3)
eventmachine (>= 1.0.3)
http_parser.rb (>= 0.6.0)
em-socksify (0.3.2)
eventmachine (>= 1.0.0.beta.4)
em-synchrony (1.0.6)
eventmachine (>= 1.0.0.beta.1)
equivalent-xml (0.6.0)
nokogiri (>= 1.4.3)
erbse (0.1.4)
@ -465,6 +476,7 @@ GEM
erubi (1.9.0)
escape_utils (1.2.1)
eventmachine (1.2.7)
eventmachine_httpserver (0.2.1)
excon (0.72.0)
execjs (2.7.0)
factory_bot (5.1.1)
@ -533,6 +545,7 @@ GEM
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http_parser.rb (0.6.0)
httpclient (2.8.3)
i18n (1.8.2)
concurrent-ruby (~> 1.0)
@ -689,6 +702,14 @@ GEM
binding_of_caller (>= 0.7)
pry (>= 0.9.11)
public_suffix (4.0.3)
puffing-billy (2.3.1)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
em-synchrony
eventmachine (~> 1.2)
eventmachine_httpserver
http_parser.rb (~> 0.6.0)
multi_json
puma (4.3.3)
nio4r (~> 2.0)
rack (2.2.2)
@ -885,6 +906,7 @@ GEM
httpclient (>= 2.4)
sys-filesystem (1.3.3)
ffi
table_print (1.5.6)
temple (0.8.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
@ -1056,6 +1078,7 @@ DEPENDENCIES
pry-rails (~> 0.3.6)
pry-rescue (~> 1.5.0)
pry-stack_explorer (~> 0.4.9.2)
puffing-billy (~> 2.3.1)
puma (~> 4.3.1)
rack-attack (~> 6.2.2)
rack-mini-profiler
@ -1100,6 +1123,7 @@ DEPENDENCIES
stringex (~> 2.8.5)
svg-graph (~> 2.1.0)
sys-filesystem (~> 1.3.3)
table_print
test-prof (~> 0.11.0)
thin (~> 1.7.2)
timecop (~> 0.9.0)

@ -1,57 +1,67 @@
<form id="enterprise-trial-form" class="form" [formGroup]="trialForm">
<div class="form--field -wide-label -required">
<label class="form--label">{{ text.label_company }}</label>
<label class="form--label" for="trial-company-name">{{ text.label_company }}</label>
<div class="form--field-container">
<div class="form--text-field-container">
<input type="text"
id="trial-company-name"
class="form--text-field"
formControlName="company">
</div>
</div>
</div>
<div class="form--field -wide-label -required">
<label class="form--label">{{ text.label_first_name }}</label>
<label class="form--label" for="trial-first-name">{{ text.label_first_name }}</label>
<div class="form--field-container">
<div class="form--text-field-container">
<input type="text"
id="trial-first-name"
class="form--text-field"
formControlName="first_name">
</div>
</div>
</div>
<div class="form--field -wide-label -required">
<label class="form--label">{{ text.label_last_name }}</label>
<label class="form--label" for="trial-last-name">{{ text.label_last_name }}</label>
<div class="form--field-container">
<div class="form--text-field-container">
<input type="text"
id="trial-last-name"
class="form--text-field"
formControlName="last_name">
</div>
</div>
</div>
<div class="form--field -wide-label -required" [ngClass]="{ '-error': eeTrialService.errorMsg }">
<label class="form--label">{{ text.label_email }}</label>
<label class="form--label" for="trial-email">{{ text.label_email }}</label>
<div class="form--field-container">
<div class="form--text-field-container"
[ngClass]="{ '-required-highlighting' : eeTrialService.errorMsg }">
<input type="email" class="form--text-field" formControlName="email" (blur)="checkMailField()">
<input type="email"
class="form--text-field"
id="trial-email"
formControlName="email" (blur)="checkMailField()">
</div>
</div>
<div *ngIf="eeTrialService.errorMsg" class="form--field-instructions">{{ eeTrialService.errorMsg }}</div>
</div>
<div class="form--field -wide-label -required">
<label class="form--label">{{ text.label_domain }}</label>
<label class="form--label" for="trial-domain-name">{{ text.label_domain }}</label>
<div class="form--field-container">
<div class="form--text-field-container">
<input type="text" class="form--text-field" formControlName="domain">
<input type="text"
id="trial-domain-name"
class="form--text-field"
formControlName="domain">
</div>
</div>
</div>
<div class="form--field -required">
<div class="form--field-container">
<label class="form--label-with-check-box -no-ellipsis">
<label class="form--label-with-check-box -no-ellipsis" for="trial-general-consent">
<div class="form--check-box-container">
<input type="checkbox"
id="trial-general-consent"
class="form--check-box"
formControlName="general_consent"
required>
@ -62,9 +72,10 @@
</div>
<div class="form--field">
<div class="form--field-container">
<label class="form--label-with-check-box -no-ellipsis">
<label class="form--label-with-check-box -no-ellipsis" for="trial-newsletter-consent">
<div class="form--check-box-container">
<input type="checkbox"
id="trial-newsletter-consent"
class="form--check-box"
formControlName="newsletter_consent">
</div>

@ -0,0 +1,217 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe 'Enterprise trial management',
type: :feature,
driver: :headless_firefox_billy do
let(:admin) { FactoryBot.create(:admin) }
let(:trial_id) { '1b6486b4-5a30-4042-8714-99d7c8e6b637' }
let(:created_body) do
{
_type: "enterprise-trial",
id: trial_id,
_links:
{
self:
{
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}"
},
details:
{
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}/details"
}
}
}
end
let(:waiting_body) do
{
_type: "error",
code: 422,
identifier: "waiting_for_email_verification",
description: "User has to confirm their email address",
_links: {
resend: {
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}/resend",
method: "POST"
},
details: {
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}/details"
}
}
}
end
let(:expired_token) do
<<~EOS
-----BEGIN OPENPROJECT-EE TOKEN-----
eyJkYXRhIjoiTE02OG5UWjJ1cTY4dnlKNWo4NEk0ZnZGdHlFcUtEU1ZxVGd5
WnBicTUzTlA5VFFOa3NSc3haOGl1KzZpXG5VTEhuQmhnWjc5c3pYRzhTV2lt
Tlg3QnpLdkh2MlFLeXFqOCtkQ2dzNHNhQUEvV21aRWZ3YmtPVExTSTBcblVY
eTYxMmFnKzY0OXVOT2dOdTZmTm5mQndoTnNZdnFGRmxDZjJZd1VQU0ROZUhQ
dWF2bDJEa3hlTTlLdlxuaWRNbC8wU3BxdWpzMVk4VjlLazhEejRJNUViQU1E
K1NOMzE1eHplOWc2MDduN2p4c3FKS3k3RVVrUTI5XG5RRG5DSTVZSTJ6bTJv
dkpaXG4iLCJrZXkiOiJSaXlnRTE0RWswdi9qVFZkOW9HRWJOcldudStQQlN2
K0xDTEVpUWZadEczY2g2djN1TERWdWVZeG8xV2NcbjRXdUFGUkdKOFEvejhn
OG01NWpyMkRKdGh6UUdoVjRYa2t4ZlN2ZUdaaUVzRWJFMmh5NzQ2cDRHNjl1
b1xuaFFOQmtqZ1FqWUZwTW9yUVBSRmhXRTNjbkp1dGFKOGU1dUVTbkZPYUFD
RDdsdkNvMUhMY2J4NWduMm96XG5NcXllbC96NytBdSt5QUNtT2poSlRaUW9L
M25ZenVuZ1FXbXJiZm93ZGUzVVN6c1lraEdyRHlBNXJSWmlcbk9TaXpqSnNE
MXBIRmZ4aVhEMnYzVlNuMWJMNXpJWFZNMDBUSFJGUHZLODVYY3IzTEVFNTZy
TVBCMytnRlxuYUptcVFUYVJOWFowamJKZ3cwNFdqUEtTbGxxaDVIVWZ5Umk2
ZjErRGxxYWlsMmcvOUZpTUpPc29QOFhhXG5IYm9oUURBY1drOHBGVE9Fci8z
NTNSdU4rejE3SklJdVdsM0Z2ZmhiQVFGZVdHQ0paR0JzTnJ4WUV3QzBcbmN1
WC80NFZKem9kcVkrRHlSUWFYNFlkcytCOUJVRnB2MHRaSnUrZmNza1MyVnc5
MGtCM3hQZTBmYTFjV1xuZTVSdHFucklFNXRQMzlEQ25YZWxwSmxGRXh5YzhX
ekZiL3BlVEVqUWtCWnM0ZUNKMzhQT1djQXh1R2s2XG5iRFppYWlEQitJSFV4
QTBuYXhSa2R5OWJvbHBNa1RBNjk1a3Q3TnhSTDJ2WU1PZFFEY3pIekFNQmpV
TE5cbnFQd3FuQWlreDgrazJqTStHWDFmTXgrUzcyb3FQTjd6M0ZqaU85S1BV
VVAveTlNMG1RV1hCZEY0bVJUR1xuM2RuUDNoOXErSHNnTkFHTUNxSHBTRzNv
L05ONTRQSCs1NCsyWk5MVDFzZ1ZubjBsQ1hVdlh0Vkt6b1hhXG5TdXlZNzBV
R3NJTUdDYmZhdnlYREVpQzU2SWtJVzNTSU1CVHVQdisxQ1J4TCtIcEEzRE5x
R09BVjVMbFFcbkUrdGNqZlNpUlVzRXcxeWkxWUZPODVEM2ZVTXdLZzFKclZB
WEV1YVdvbjUxMzNVRnZNZjBNbFhkSUQ1L1xuNEtXczNGeEdmWVJJRUQ5VlhR
eFNYdEQ3cWYzSlFFbHdHSGVMdUtVYkRmMWEzTEVKMUFKb2FOV0phcG9xXG4v
QlU3ZHJoM28zSDFXYVBpeUhpUlQrVExTa2cxUXhxY2p0eUVuK1JiNnBKVmwr
eVJXWHMrR1pkcGFDY09cbnh2ZWRIam12NzNsUjc5WFpVNVh6UlhwY3E1d1pm
T2FVaHZ1NHllQysyR0FpYlIrcGowbTA0UzRXbjI0elxucjUvZGtnSG5Xek9B
V2lZb0MxOEZpckhTSnVGM1FHWHJUK1JyT2c4QVdwTDlHMGZQQlpveTJvNFZj
V004XG51UXJvSDUwT3Rtcm00cW53QUU3TEFyc3g3bWxOblBGMmpyejZMeWkz
UlhDN1ZrSE9FVXhiUHNjZHJiRlhcbkd3cTlvNU5LNi9sb2RVTTAzeklyaTBs
TVdKSlpUU3BNMnVzU0VxWUpoS05uSGI1a3lYcy9MRkhOWW05c1xuN2hBOVdS
RUxWQi9Tc2x5RjJQczNzSHJQaGtZM1BGZElSeU9Kb2JxdnZoaUpPTVA5dDVu
MUxUeTFjbkhGXG5DbTBDM0U1bWFjTi9hOE5OSXk2dGhia3JJVE5XK2I4K2Jw
VDN3OGkxSDVYNCtodlJ5T1g0Y0JEVWhNN2pcbnN3Wkw0citmWlRmaGlNQkZi
K2NmSUZ0U2lyMVBpdz09XG4iLCJpdiI6IjFxbEZqRWM4QzcrMjg4QWR6cXdL
OEE9PVxuIn0=
-----END OPENPROJECT-EE TOKEN-----
EOS
end
let (:confirmed_body) do
{
_type: "enterprise-trial",
id: trial_id,
token: expired_token,
token_retrieved: false,
_links: {
self: {
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}"
}
}
}
end
let(:mail_in_use_body) do
{
_type: "error",
code: 422,
identifier: "user_already_created_trial",
description: "Each user can only create one trial."
}
end
before do
login_as(admin)
visit enterprise_path
end
def fill_out_modal(mail: 'foo@foocorp.example')
fill_in 'Company', with: 'Foo Corp.'
fill_in 'First name', with: 'Foo'
fill_in 'Last name', with: 'Bar'
fill_in 'Email', with: mail
fill_in 'Domain', with: 'foo.example.com'
find('#trial-general-consent').check
end
it 'blocks the request assuming the mail was used' do
proxy.stub('https://augur.openproject-edge.com:443/public/v1/trials', method: 'post')
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 422, body: mail_in_use_body.to_json)
find('.button', text: 'Start free trial').click
fill_out_modal
find('.button:not(:disabled)', text: 'Submit').click
expect(page).to have_selector('.form--field.-error #trial-email')
expect(page).to have_text 'Each user can only create one trial.'
expect(page).to have_no_text 'email sent - waiting for confirmation'
end
it 'can request a new trial' do
proxy.stub('https://augur.openproject-edge.com:443/public/v1/trials', method: 'post')
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 200, body: created_body.to_json)
proxy.stub("https://augur.openproject-edge.com:443/public/v1/trials/#{trial_id}")
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 422, body: waiting_body.to_json)
find('.button', text: 'Start free trial').click
fill_out_modal
find('.button:not(:disabled)', text: 'Submit').click
expect(page).to have_text 'foo@foocorp.example'
expect(page).to have_text 'email sent - waiting for confirmation'
# Stub resend method
proxy.stub("https://augur.openproject-edge.com:443/public/v1/trials/#{trial_id}/resend")
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 200, body: waiting_body.to_json)
find('.op-modal--modal-body #resend-link', text: 'Resend').click
expect(page).to have_text 'Email has been resent.'
expect(page).to have_text 'foo@foocorp.example'
expect(page).to have_text 'email sent - waiting for confirmation'
# Stub the proxy to a successful return
# which marks the user has confirmed the mail link
proxy.stub("https://augur.openproject-edge.com:443/public/v1/trials/#{trial_id}")
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 200, body: confirmed_body.to_json)
# Wait until the next request
expect(page).to have_selector '.status--confirmed', text: 'confirmed', wait: 20
# advance to video
click_on 'Continue'
# advance to close
click_on 'Continue'
expect(page).to have_selector('.flash.notice', text: 'Successful update.', wait: 10)
expect(page).to have_selector('.attributes-key-value--value-container', text: 'OpenProject Test')
expect(page).to have_selector('.attributes-key-value--value-container', text: '01/01/2020')
expect(page).to have_selector('.attributes-key-value--value-container', text: '01/02/2020')
expect(page).to have_selector('.attributes-key-value--value-container', text: '5')
# Generated expired token has different mail
expect(page).to have_selector('.attributes-key-value--value-container', text: 'info@openproject.com')
end
end

@ -33,6 +33,7 @@ require 'rspec/rails'
require 'shoulda/matchers'
require 'test_prof/recipes/rspec/before_all'
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end

@ -6,9 +6,7 @@ if ENV['CI']
::Webdrivers::Chromedriver.update
end
def register_chrome_headless(language)
name = :"chrome_headless_#{language}"
def register_chrome_headless(language, name: :"chrome_headless_#{language}")
Capybara.register_driver name do |app|
options = Selenium::WebDriver::Chrome::Options.new
@ -37,6 +35,12 @@ def register_chrome_headless(language)
options.add_preference(:browser, set_download_behavior: { behavior: 'allow' })
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
loggingPrefs: { browser: 'ALL' }
)
yield options, capabilities
client = Selenium::WebDriver::Remote::Http::Default.new
client.read_timeout = 180
client.open_timeout = 180
@ -44,6 +48,7 @@ def register_chrome_headless(language)
driver = Capybara::Selenium::Driver.new(
app,
browser: :chrome,
desired_capabilities: capabilities,
http_client: client,
options: options
)
@ -70,26 +75,10 @@ register_chrome_headless 'en'
register_chrome_headless 'de'
# Register mocking proxy driver
Capybara.register_driver :headless_chrome_billy do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
acceptInsecureCerts: true,
loggingPrefs: { browser: 'ALL' }
)
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--window-size=1920,1080')
options.add_argument('--headless')
options.add_argument('--disable-gpu')
register_chrome_headless 'en', name: :headless_chrome_billy do |options, capabilities|
options.add_argument("--proxy-server=#{Billy.proxy.host}:#{Billy.proxy.port}")
options.add_argument('--proxy-bypass-list=127.0.0.1;localhost')
Capybara::Selenium::Driver.new app,
browser: :chrome,
options: options,
desired_capabilities: capabilities,
driver_opts: {
log_path: Rails.root.join('log/chromedriver.log').to_s,
verbose: true,
}
capabilities[:acceptInsecureCerts] = true
end
Capybara.javascript_driver = :headless_chrome_billy

@ -7,9 +7,7 @@ if ENV['CI']
end
def register_firefox_headless(language)
name = :"firefox_headless_#{language}"
def register_firefox_headless(language, name: :"firefox_headless_#{language}")
require 'selenium/webdriver'
Capybara.register_driver name do |app|
@ -38,6 +36,12 @@ def register_firefox_headless(language)
options = Selenium::WebDriver::Firefox::Options.new(profile: profile)
capabilities = Selenium::WebDriver::Remote::Capabilities.firefox(
loggingPrefs: { browser: 'ALL' }
)
yield profile, options, capabilities
unless ActiveRecord::Type::Boolean.new.cast(ENV['OPENPROJECT_TESTING_NO_HEADLESS'])
options.args << "--headless"
end
@ -49,6 +53,8 @@ def register_firefox_headless(language)
app,
browser: :firefox,
options: options,
desired_capabilities: capabilities,
http_client: client,
)
@ -64,6 +70,17 @@ register_firefox_headless 'en'
# Register german locale for custom field decimal test
register_firefox_headless 'de'
# Register mocking proxy driver
register_firefox_headless 'en', name: :headless_firefox_billy do |profile, options, capabilities|
profile.assume_untrusted_certificate_issuer = false
profile.proxy = Selenium::WebDriver::Proxy.new(
http: "#{Billy.proxy.host}:#{Billy.proxy.port}",
ssl: "#{Billy.proxy.host}:#{Billy.proxy.port}")
capabilities[:accept_insecure_certs] = true
end
# Resize window if firefox
RSpec.configure do |config|
config.before(:each, driver: Proc.new { |val| val.to_s.start_with? 'firefox_headless_' }) do

@ -0,0 +1,39 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
# puffing-billy is a gem that creates a middleman proxy between the browser controlled
# by capybara/selenium and the spec execution.
#
# This allows us to stub requests to external APIs to guarantee responses regardless of
# their availability.
#
# In order to use the proxied server, you need to use `driver: headless_firefox_billy` in your examples
#
# See https://github.com/oesmith/puffing-billy for more information
require 'billy/capybara/rspec'

@ -36,17 +36,17 @@ RSpec.configure do |config|
WebMock.disable!
end
# When we enable webmock, no connections other than stubbed ones are allowed.
# We will exempt local connections from this block, since selenium etc.
# uses localhost to communicate with the browser.
# Leaving this off will randomly fail some specs with WebMock::NetConnectNotAllowedError
WebMock.disable_net_connect!(allow_localhost: true)
config.around(:example, webmock: true) do |example|
begin
# When we enable webmock, no connections other than stubbed ones are allowed.
# We will exempt local connections from this block, since selenium etc.
# uses localhost to communicate with the browser.
# Leaving this off will randomly fail some specs with WebMock::NetConnectNotAllowedError
WebMock.disable_net_connect!(allow_localhost: true)
WebMock.enable!
example.run
ensure
WebMock.allow_net_connect!
WebMock.disable!
end
end

Loading…
Cancel
Save