I want to test a rails job which call an endpoint of hubspot API (GET /crm/v3/owners/{ownerId}) and update a record with infos of the request result.
The problem is that I use this gem as an API wrapper and my before block seems like ignored because the result shows that the API call can't give me an owner object with this owner_id (the one given as parameter of attributes is obviously fake). A before block is supposed to override the "normal" response of the controller isn't it ?
I really don't know what I am doing wrong ..
For more context:
My job code
module Hubspots
module Contracts
class UpdateJob < BaseJob
queue_as :high_priority
def perform(attributes)
contract = Contract.find_by(hubspot_sales_deal_id: attributes[:hubspot_sales_deal_id])
return if contract.nil?
deal_owner = client.crm.owners.owners_api.get_by_id(owner_id: attributes[:hubspot_tailor_deal_owner],
id_property: 'id', archived: false)
attributes[:hubspot_tailor_deal_owner] = get_owner_name(deal_owner)
contract.update!(attributes)
end
private
def get_owner_name(hubspot_owner_object)
"#{hubspot_owner_object.last_name.upcase} #{hubspot_owner_object.first_name.capitalize}"
end
end
end
end
My test code
RSpec.describe Hubspots::Contracts::UpdateJob, type: :job do
let!(:job) { described_class.new }
let(:perform) { job.perform(attributes) }
let!(:contract) { create(:contract, hubspot_sales_deal_id: 123) }
let!(:attributes) do
{ hubspot_tailor_deal_id: 456, hubspot_tailor_deal_owner: 876, hubspot_sales_deal_id: 123 }
end
let!(:deal_owner_api) { Hubspot::Client.new(access_token: ENV['HUBSPOT_ACCESS_TOKEN']).crm.owners.owners_api }
let!(:deal_owner_properties) { { last_name: 'Doe', first_name: 'John' } }
before do
allow(deal_owner_api).to receive(:get_by_id).and_return(deal_owner_properties)
end
describe '#perform' do
it 'updates contract' do
expect { perform }.to change { contract.reload.hubspot_tailor_deal_owner }.from(nil)
.to('DOE John')
end
end
end
Test result
I try to rtfm on google but I didn't find the solution yet (I'm always bad for rtfm btw)
Ok my CTO finaly gave me the solution by using the Webmock gem
The code snippet :
before do
stub_request(:get, 'https://api.hubapi.com/crm/v3/owners/876?archived=false&idProperty=id')
.with(headers: { 'Authorization' => "Bearer #{ENV['HUBSPOT_ACCESS_TOKEN']}" }).to_return(status: 200, body: {
firstName: 'John',
lastName: 'Doe'
}.to_json, headers: {})
end
describe '#perform' do
it 'updates contract' do
expect { perform }.to change { contract.reload.hubspot_tailor_deal_owner }.from(nil)
.to('DOE John')
end
end
Related
I am trying to write a test to ensure that my service, WeeklyReportCardService, is instantiated and that it's method :send_weekly_report_card_for_repositioning is called.
Here's the controller:
def update
Audited.audit_class.as_user($user) do
if #check_in.update(check_in_params)
client = Client.find_by(id: check_in_params[:client_id])
if #check_in.repositioning.present? && #check_in.weigh_in.present? && #check_in.client&.location&.name == "World Wide"
# I see this in the console so the if statement returns true
p "hitting send!!"
WeeklyReportCardService.new.send_weekly_report_card_for_repositioning(#check_in.repositioning)
end
render json: #check_in, status: :ok, serializer: API::CheckInsIndexSerializer
else
render json: #check_in.errors.full_messages, sattus: :unprocessable_entity
end
end
end
Here's my test:
RSpec.describe API::CheckInsController, type: :request do
fit "should send if the client's location is World Wide" do
program = create(:program, :with_client)
worldwide = create(:location, name: "World Wide")
program.client.update(location_id: worldwide.id)
check_in = create(:check_in, client_id: program.client.id, program_id: program.id)
create(:repositioning, check_in_id: check_in.id)
create(:weigh_in, check_in_id: check_in.id)
url = root_url[0..-2] + api_check_in_path(check_in.id) + "?sendReportCardEmail=true"
put url, params: { check_in: {type_of_weighin: 'standard'}}, headers: { "HTTP_AUTHENTICATION": #token }
expect_any_instance_of(WeeklyReportCardService).to receive(:send_weekly_report_card_for_repositioning)
end
end
and the error I see is:
Failure/Error: DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure }
Exactly one instance should have received the following message(s) but didn't: send_weekly_report_card_for_repositioning
What else do I need to do to ensure that function is called?
You're doing it in the wrong order. You need to set the expectation first before the method is expected to be called:
expect_any_instance_of(WeeklyReportCardService).to receive(:send_weekly_report_card_for_repositioning)
put url, params: { check_in: {type_of_weighin: 'standard'}}, headers: { "HTTP_AUTHENTICATION": #token }
If you need to set the expecations afterwards you need to replace the method or object with a spy which is useful if you prefer the arrange-act-assert (or given-when-then)
pattern for structuring tests.
You should also note that the use of any instance is strongly discouraged and you can avoid it by providing a simple class method:
class WeeklyReportCardService
def self.send_weekly_report_card_for_repositioning(...)
new.send_weekly_report_card_for_repositioning(...)
end
end
RSpec.describe API::CheckInsController, type: :request do
it "should send if the client's location is World Wide" do
expect(WeeklyReportCardService).to receive(:send_weekly_report_card_for_repositioning)
put url, params: { check_in: {type_of_weighin: 'standard'}}, headers: { "HTTP_AUTHENTICATION": #token }
end
end
Or alternatively by stubbing the WeeklyReportCardService#new method to return a mock or spy.
I've created the following service
class ApiCustomers < ApiBase
def initialize(url: nill)
url = url.presence || ENV.fetch("CUSTOMERS_API_APP_NAME")
super(url)
end
def find_customer_apps(customer_id, params=nil)
begin
customer_url = "#{#url}/customers/#{customer_id}/applications"
# call api-customers
if params.nil?
response = connection.get(customer_url)
else
response = connection.get(customer_url, params)
end
rescue StandardError => e
Rails.logger.error "Error fetching API-CUSTOMERS Applications: #{e}"
raise e
end
return response.body["data"]
end
end
Now, I want to test that with the following code:
require "rails_helper"
RSpec.describe ApiCustomers do
let(:customer_id) { SecureRandom.uuid }
let(:customers_client) { ApiCustomers.new(url: "api_customers.local") }
context "#find_customer_apps" do
let(:app) { double("application", id: 123, name: "App", customer_id: "123", supported: false, additional_notes: "Test") }
let(:success_response) { OpenStruct.new({ status: 200, body: { "data": app } }) }
let(:bad_request) { OpenStruct.new({ status: 400, body: {"error"=>"invalid call"} }) }
describe "with valid params" do
before do
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(success_response)
end
it "returns customer applications" do
applications_payload = customers_client.find_customer_apps(account_id, nil)
expect(applications_payload).not_to be_nil
end
end
describe "with invalid params" do
before do
allow_any_instance_of(Faraday::Connection).to receive(:get).and_raise(CustomErrors::BadRequest)
end
it "raises an error" do
expect { customers_client.find_customer_apps(nil, nil) }.to raise_error(CustomErrors::BadRequest)
end
end
end
end
The test with invalid params is working, but for some reason the one with valid params is failing.
If I print the success_response, this is being created properly. I do not know what I am getting nil in my applications_paylod. What I am missing? I thought this line should cover what I am doing: allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(success_response)
Thanks in advance.
I found the issue:
In my stub:
let(:success_response) { OpenStruct.new({ status: 200, body: { "data": app } }) }
I am creating an Struct, so, I decided to change the code of my method in order to return: response.body[:data]
instead of response.body["data"]
I'm writing some tests using FactoryGirl and Rspec.
spec/factories/students.rb:
FactoryGirl.define do
factory :student do
end
factory :student_with_profile_and_identity, class: 'Student' do
after(:create) do |student|
create(:profile, profileable: student)
create(:student_identity, student: student)
end
end
end
spec/factories/profiles.rb:
FactoryGirl.define do
factory :profile do
birthday { Faker::Date.birthday(15, 150) }
sequence(:email) { |i| "profile_#{i}#email.com" }
first_name { Faker::Name.first_name }
last_name { Faker::Name.first_name }
password { Faker::Internet.password(6, 72, true, true) }
end
end
spec/factories/student_identities.rb:
FactoryGirl.define do
factory :student_identity do
provider { ['facebook.com', 'google.com', 'twitter.com'].sample }
uid { Faker::Number.number(10) }
end
end
spec/requests/authorizations_spec.rb:
require 'rails_helper'
RSpec.describe 'Authorizations', type: :request do
describe 'POST /v1/authorizations/sign_in' do
let!(:student) { create(:student_with_profile_and_identity) }
context 'when the request is valid' do
subject do
post '/v1/authorizations/sign_in',
params: credentials
end
context "user signs up via social network" do
let(:credentials) do
{
authorization: {
student: {
profile_attributes: {
email: student.profile.email
},
student_identities_attributes: {
provider: student.student_identities[0].provider,
uid: student.student_identities[0].uid
}
}
}
}
end
it 'returns an authentication token' do
subject
p "1 student.profile.inspect #{student.profile.inspect}"
expect(json['token']).to(be_present)
end
end
context 'when the user has already an account' do
let(:credentials) do
{
authorization: {
email: student.profile.email,
password: student.profile.password
}
}
end
it 'returns an authentication token' do
p "2 student.profile.inspect #{student.profile.inspect}"
subject
expect(json['token']).to(be_present)
end
end
end
end
end
Almost all tests are passing... the problem is that:
It's creating a new student in every context. I'd expect the let!(:student) { ... } to be something like "singleton", in other words, once it's created/defined here let!(:student) { create(:student_with_profile_and_identity) } it won't be called anymore.
Ex: the logs are like this:
"1 student.profile.inspect #<Profile id: 1, email: \"profile_1#email.com\", profileable_type: \"Student\", profileable_id: 1>"
"2 student.profile.inspect #<Profile id: 2, email: \"profile_2#email.com\", profileable_type: \"Student\", profileable_id: 2>"
While I'd expect the instances to be the same.
Am I missing something?
In RSpec, let and let! are the same thing, except that let is lazy and let! is eager:
Use let to define a memoized helper method. The value will be cached across multiple calls in the same example but not across examples.
Note that let is lazy-evaluated: it is not evaluated until the first time the method it defines is invoked. You can use let! to force the method's invocation before each example.
If you want something to persist through all examples, you can use a before hook...before(:context) sounds like it might be what you're wanting. You might be able to setup a helper method that memoizes in a before block, to avoid having to use an instance variable everywhere (per this comment):
def student
#student ||= create(:student_with_profile_and_identity)
end
before(:context) do
student # force student creation
end
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
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