rspec 3.4 test controller concern with response.body.read - ruby-on-rails

I have the following controller concern that is used for authentication:
module ValidateEventRequest
extend ActiveSupport::Concern
def event_request_verified?(request)
sha256 = OpenSSL::Digest::SHA256.new
secret = app_client_id
body = request.body.read
signature = OpenSSL::HMAC.hexdigest(sha256, secret, body)
([signature] & [request.headers['X-Webhook-Signature'], request.headers['X-Api-Signature']]).present?
end
private
def app_client_id
ENV['APP_CLIENT_ID']
end
end
So far I have the following Rspec Test setup to hit this:
RSpec.describe ValidateEventRequest, type: :concern do
let!(:current_secret) { SecureRandom.hex }
describe '#event_request_verified?' do
it 'validates X-Webhook-Signature' do
# TBD
end
it 'validates X-Api-Signature' do
# TBD
end
end
end
I started out with stubbing the request, then mocking and stubbing, and now I am down to scrapping what I have and seeking assistance. 100% coverage is important to me and I am looking for some pointers on how to structure tests that cover this 100%.

object_double is handy for testing concerns:
require 'rails_helper'
describe MyClass do
subject { object_double(Class.new).tap {|c| c.extend MyClass} }
it "extends the subject" do
expect(subject.respond_to?(:some_method_in_my_class)).to be true
# ...
Then you can test subject like any other class. Of course you need to pass in the appropriate arguments when testing methods, which may mean creating additional mocks -- in your case a request object.

Here is how I solved this issue, and I am open to ideas:
RSpec.describe ValidateApiRequest, type: :concern do
let!(:auth_secret) { ENV['APP_CLIENT_ID'] }
let!(:auth_sha256) { OpenSSL::Digest::SHA256.new }
let!(:auth_body) { 'TESTME' }
let(:object) { FakeController.new }
before(:each) do
allow(described_class).to receive(:secret).and_return(auth_secret)
class FakeController < ApplicationController
include ValidateApiRequest
end
end
after(:each) do
Object.send :remove_const, :FakeController
end
describe '#event_request_verified?' do
context 'X-Api-Signature' do
it 'pass' do
request = OpenStruct.new(headers: { 'X-Api-Signature' => OpenSSL::HMAC.hexdigest(auth_sha256, auth_secret, auth_body) }, raw_post: auth_body)
expect(object.event_request_verified?(request)).to be_truthy
end
it 'fail' do
request = OpenStruct.new(headers: { 'X-Api-Signature' => OpenSSL::HMAC.hexdigest(auth_sha256, 'not-the-same', auth_body) }, raw_post: auth_body)
expect(object.event_request_verified?(request)).to be_falsey
end
end
context 'X-Webhook-Signature' do
it 'pass' do
request = OpenStruct.new(headers: { 'X-Webhook-Signature' => OpenSSL::HMAC.hexdigest(auth_sha256, auth_secret, auth_body) }, raw_post: auth_body)
expect(object.event_request_verified?(request)).to be_truthy
end
it 'fail' do
request = OpenStruct.new(headers: { 'X-Webhook-Signature' => OpenSSL::HMAC.hexdigest(auth_sha256, 'not-the-same', auth_body) }, raw_post: auth_body)
expect(object.event_request_verified?(request)).to be_falsey
end
end
end
end

Related

How make stub on request with VCR?

How can I do stub on request with VCR?
The problem is that real request is made in the test, which I want to stub.
RSpec.describe CreditRegisterLoader do
describe ".call" do
it "should create credit institutions", :vcr do
Timecop.freeze(2020, 3, 25, 13, 0, 0) do
expect { described_class.new.call }.to change { CreditInstitution.count }.by(4965)
end
end
end
end
Also here is code of my class CreditRegisterLoader which I want to test:
class CreditRegisterLoader < ApplicationService
def initialize
#timestamp = (Time.now - 1.minute).to_i.to_s
end
def call
sai_response = get_credit_institutions
unless sai_response
Airbrake.notify("invalid_sai_response")
return
end
begin
CreditInstitutionUpdater.new(JSON.parse(sai_response.body)).create
rescue => error
Airbrake.notify(error)
end
end
private
def get_credit_institutions
RestClient::Request.execute(
method: :post,
url: "https://sai.dpl.europa.eu/register/api/search/entities?t=#{#timestamp}",
headers: {
"Content-Type" => "application/json",
"Accept" => "application/json",
},
payload: JSON.generate({"$and": [{"_messagetype": "SAIDPL"}]})
)
end
end
I would suggest the following solution
RSpec.describe CreditRegisterLoader do
describe ".call" do
let(:response) { OpenStruct.new(body: File.read("yours fixtures in json format")) }
context "Failure flow" do
it "should notify Airbrake with error" do
error = StandardError.new("Bad Request")
expect(RestClient::Request).to receive(:execute).and_return(response)
expect_any_instance_of(CreditInstitutionUpdater).to receive(:create).and_raise(error)
expect(Airbrake).to receive(:notify).with(error)
subject.call
end
it "should notify Airbrake with invalid_sai_response" do
expect(subject).to receive(:get_credit_institutions).and_return(nil)
expect(Airbrake).to receive(:notify).with("invalid_sai_response")
subject.call
end
end
context "Successfully flow" do
it "should creates credit institutions" do
expect(RestClient::Request).to receive(:execute).and_return(response)
expect { subject.call }.to change { CreditInstitution.count }.by(2)
fixtures_response = JSON.parse(response.body)
end
end
end
end

Rspec for Shopify controllers? Stuck on a 302 for authentication

My project is a Rails 5.2 app, running Ruby 2.6, and uses the shopify_gem and factory_bot_rails.
I have a controller that inherits from ShopifyController. My unit tests for controllers are stuck at a 302. I'm unable to figure out how to get past authentication...
I've tried these tutorials and other links, but no luck:
http://www.codeshopify.com/blog_posts/testing-shopify-authenticated-controllers-with-rspec-rails
https://community.shopify.com/c/Shopify-APIs-SDKs/Testing-a-Rails-app-created-through-shopify-app-gem/td-p/337337
https://github.com/Shopify/shopify_app/issues/445
https://github.com/Shopify/shopify_app/issues/731
My controller test is below
require 'rails_helper'
describe OnboardingController, type: :controller do
before do
shop = FactoryBot.create(:shop)
request.env['rack.url_scheme'] = 'https'
#request.session[:shopify] = shop.id
#request.session[:shopify_domain] = shop.shopify_domain
end
it 'onboards correctly', :focus do
get :onboard_completed
expect(response).to have_http_status(:success)
end
end
I was also playing with this code, but it failed (errors in comments):
module ShopifyHelper
def login(shop)
OmniAuth.config.test_mode = true
OmniAuth.config.add_mock(:shopify,
provider: 'shopify',
uid: shop.shopify_domain,
credentials: { token: shop.shopify_token })
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:shopify]
get "/auth/shopify" # this leads to a UrlGenerationError
follow_redirect! # this is an undefined method. Seems to be a MiniTest thing
end
end
require 'rails_helper'
RSpec.describe "Home", type: :request do
def login(shop)
OmniAuth.config.test_mode = true
OmniAuth.config.add_mock(:shopify,
provider: 'shopify',
uid: shop.shopify_domain,
credentials: { token: shop.shopify_token })
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:shopify]
get "/auth/shopify"
follow_redirect!
#request.session[:shopify] = shop.id
#request.session[:shopify_domain] = shop.shopify_domain
end
describe "GET /" do
it "works!" do
shop = Shop.first || create(:shop)
login(shop)
get root_path
shop.with_shopify!
expect(assigns(:products)).to eq ShopifyAPI::Product.find(:all, params: { limit: 10 })
expect(response).to render_template(:index)
expect(response).to have_http_status(200)
end
end
end
Something like this works for me, your getting the errors in your function probably because you do not have get and follow_redirect! functions defined in your ShopifyHelper module context.
Reference: http://www.codeshopify.com/blog_posts/testing-shopify-authenticated-controllers-with-rspec-rails
This ended up being the working solution
require 'rails_helper'
describe WizardController, type: :controller do
before do
shop = FactoryBot.create(:shop)
request.env['rack.url_scheme'] = 'https'
allow(shop).to receive(:wizard_completed?).and_return(false)
allow(Shop).to receive(:current_shop).and_return(shop)
# #note: my original code had "session[:shopify]" of "session[:shop]", which was the error
session[:shop_id] = shop.id
session[:shopify_domain] = shop.shopify_domain
end
it 'enter test here', :focus do
get :wizard
expect(response).to have_http_status(:success)
end
end
This worked for me:
# File: spec/support/request_helper.rb
def shopify_login(shop)
OmniAuth.config.test_mode = true
OmniAuth.config.add_mock(:shopify, provider: 'shopify', uid: shop.myshopify_domain,
credentials: { token: shop.api_token })
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:shopify]
get "/auth/shopify/callback?shop=#{shop.myshopify_domain}"
follow_redirect!
#request.session[:shopify] = shop.shopify_id
#request.session[:shop_id] = shop.id
#request.session[:shopify_domain] = shop.myshopify_domain
end
Btw, testing controllers are deprecated in favour of requests.
RSpec.describe 'ShopsController', type: :request do
let(:shop) { FactoryBot.build :shop }
let(:plan) { FactoryBot.build :enterprise_plan }
let(:subscription) { FactoryBot.create :subscription, shop: shop, plan: plan }
describe 'GET#product_search' do
it 'returns a successful 200 response for listing action do' do
VCR.use_cassette('shop-search-product', record: :new_episodes) do
new_subscrip = subscription
shopify_login(new_subscrip.shop)
get product_search_path, { params: { query: 'bike' } }
json = JSON.parse(response.body)
expect(response).to be_successful
expect(json.length).to eq(7)
end
end
end
Remember to setup "admin { true }" in your shop's FactoryBot if you are using the 'shopify_app' gem.

How to assert that a method call was not made, without any_instance?

I have a class, that in one situation should call :my_method, but in another situation must not call method :my_method. I would like to test both cases. Also, I would like the test to document the cases when :my_method should not be called.
Using any_instance is generally discouraged, so I would be happy to learn a nice way to replace it.
This code snippet is a reduced example on what I kind of test I would like to write.
class TestSubject
def call
call_me
end
def call_me; end
def never_mind; end
end
require 'rspec'
spec = RSpec.describe 'TestSubject' do
describe '#call' do
it 'calls #call_me' do
expect_any_instance_of(TestSubject).to receive(:call_me)
TestSubject.new.call
end
it 'does not call #never_mind' do
expect_any_instance_of(TestSubject).not_to receive(:never_mind)
TestSubject.new.call
end
end
end
spec.run # => true
It works, but uses expect_any_instance_of method, which is not recommended.
How to replace it?
I'll do somehting like that
describe TestSubject do
describe '#call' do
it 'does not call #something' do
subject = TestSubject.new
allow(subject).to receive(:something)
subject.call
expect(subject).not_to have_received(:something)
end
end
end
Hope this helped !
This is how I normally unit-test. I updated the code to support other possible questions you (or other readers) may have in the future.
class TestSubject
def call
some_call_me_value = call_me
call_you(some_call_me_value)
end
def call_me; end
def call_you(x); end
def never_mind; end
class << self
def some_class_method_a; end
def some_class_method_b(x, y); end
end
end
require 'rspec'
spec = RSpec.describe TestSubject do
context 'instance methods' do
let(:test_subject) { TestSubject.new }
describe '#call' do
let(:args) { nil }
let(:mocked_call_me_return_value) { 'somecallmevalue' }
subject { test_subject.call(*args) }
before do
allow(test_subject).to receive(:call_me) do
mocked_call_me_return_value
end
end
it 'calls #call_me' do
expect(test_subject).to receive(:call_me).once
subject
end
it 'calls #call_you with call_me value as the argument' do
expect(test_subject).to receive(:call_you).once.with(mocked_call_me_return_value)
subject
end
it 'does not call #never_mind' do
expect(test_subject).to_not receive(:never_mind)
subject
end
it 'calls in order' do
expect(test_subject).to receive(:call_me).once.ordered
expect(test_subject).to receive(:call_you).once.ordered
subject
end
end
describe '#call_me' do
let(:args) { nil }
subject { test_subject.call_me(*args) }
# it ...
end
describe '#call_you' do
let(:args) { nil }
subject { test_subject.call_you(*args) }
shared_examples_for 'shared #call_you behaviours' do
it 'calls your phone number'
it 'creates a Conversation record'
end
# just an example of argument-dependent behaviour spec
context 'when argument is true' do
let(:args) { [true] }
it 'does something magical'
it_behaves_like 'shared #call_you behaviours'
end
# just an example of argument-dependent behaviour spec
context 'when argument is false' do
let(:args) { [false] }
it 'does something explosive'
it_behaves_like 'shared #call_you behaviours'
end
end
end
context 'class methods' do
let(:args) { nil }
describe '#some_class_method_a' do
let(:args) { nil }
subject { TestSubject.some_class_method_a(*args) }
# it ...
end
describe '#some_class_method_b' do
let(:args) { [1, 2] }
subject { TestSubject.some_class_method_b(*args) }
# it ...
end
end
end
spec.run # => true
Do not test if some method was called or wasn't.
This will tight your tests to the implementation details and will force you to change tests every time you refactor(change implementation details without changing the behaviour) your class under test.
Instead test against return value or changed application state.
It is difficult come up with the example, you didn't provide enough context about the class under the test.
class CreateEntity
def initialize(name)
#name = name
end
def call
if company_name?(#name)
create_company
else
create_person
end
end
def create_person
Person.create!(:name => #name)
end
def create_company
Company.create!(:name => #name)
end
end
# tests
RSpec.describe CreateEntity do
let(:create) { CreateEntity.new(name).call }
describe '#call' do
context 'when person name is given' do
let(:name) { 'Firstname Lastname' }
it 'creates a person' do
expect { create }.to change { Person.count }.by(1)
end
it 'do not create a company' do
expect { create }.not_to change { Company.count }
end
end
context 'when company name is given' do
let(:name) { 'Name & Sons Ltd' }
it 'creates a company' do
expect { create }.to change { Company.count }.by(1)
end
it 'do not create a person' do
expect { create }.not_to change { Person.count }
end
end
end
end
With tests above I would be able to change how CreateEntity.call method implemented without changing tests as far as behaviour remain same.

RSpec Helper Parameters Issue

I'm trying to test the following code:
module ApplicationHelper
def current_book
Book.find(params[:id])
end
end
using the following test with RSpec:
RSpec.describe ApplicationHelper, :type => :helper do
describe "#current_book" do
book_1 = create(:book)
params = {}
params[:id] = book_1.id
expect(helper.current_book).to eq(book_1)
end
end
But for some reason the params[:id] parameter isn't being passed in properly. Any suggestions with this?
You need to stub the params:
RSpec.describe ApplicationHelper, type: :helper do
describe "#current_book" do
let(:first_book) { create(:book) }
before(:all) { helper.stub!(:params).and_return(id: 1) }
it "returns a book with a matching id" do
expect(helper.current_book).to eq(first_book)
end
end
end
Here another way of stubbing params. I think this requires rspec 3 can't remember for sure.
context 'path is a route method' do
before { allow(helper).to receive(:params).and_return(order_by: { updated_at: :desc }) }
subject { helper.sortable_link_to('Created At', order_by: :created_at) }
it { is_expected.to match /comments/ }
it { is_expected.to match /\?order_by/}
it { is_expected.to match /\?order_by%5Bupdated_at%5D=asc/}
end

Can't stub Curl method

Here's my test:
# Custom validation method tests
describe "#ticker_symbol" do
before(:each) do
o = OpenStruct.new(:body => '')
Curl::Easy.any_instance.stub(:get).and_return(o)
end
it "should add an error" do
subject
end
end
And the relevant part of my model:
# Custom validation methods
def ticker_symbol
apiresponse = Curl.get("https://www.google.com/finance/info?infotype=infoquoteall&q=" + ticker)
debugger
if apiresponse.body == ''
errors.add(:ticker, "must be valid")
end
end
For some reason, apiresponse isn't what it should be:
apiresponse
#<Curl::Easy https://www.google.com/finance/info?infotype=infoq>
Any idea why my stubbing isn't working?
# Custom validation method tests
describe "#ticker_symbol" do
let(:stubbed_response) { OpenStruct.new(:body => '') }
before(:each) do
Curl.stub(:get).and_return stubbed_response
end
it "should add an error" do
subject
end
end

Resources