My team has been stumped on this issue for some time now and are at a loss as to where to try next. The spec below works correctly when run individually, however, when we run this in our suite via bundle exec ./bin/rspec spec
these two tests fail every single time:
- GET /external-products/:id/deals
- GET /external-products/search/deals
We have tried numerous different ways to approach this issue and I am starting to suspect something else outside of the said spec. SO I have to turn to the Stack gods, and plea that someone out there has a better approach or even a better question to ask this problem.
Rspec Errors:
8) Retailigence Products and Locations GET /external-products/search/deals Search a given region for related deals by query string
Failure/Error: expect(response_body).to have_json_type(Integer).at_path('deals/0/id')
JsonSpec::MissingPath:
Missing JSON path "deals/0/id"
# ./spec/features/external_products_spec.rb:151:in `block (3 levels) in <top (required)>'
# -e:1:in `<main>'
9) Retailigence Products and Locations GET /external-products/:id/deals Search a given region for deals related to a particular product
Failure/Error: expect(response_body).to have_json_type(Integer).at_path('deals/0/id')
JsonSpec::MissingPath:
Missing JSON path "deals/0/id"
# ./spec/features/external_products_spec.rb:105:in `block (3 levels) in <top (required)>'
# -e:1:in `<main>'
Here is our spec_helper.rb:
require 'rubygems'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rspec/rails'
require 'email_spec'
require 'pry'
require 'rspec_api_documentation/dsl'
require 'sunspot/rails/spec_helper'
require 'sunspot_test/rspec'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join('spec/concerns/**/*.rb')].each { |f| require f }
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
# Checks for pending migrations before tests are run.
# If you are not using ActiveRecord, you can remove this line.
ActiveRecord::Migration.maintain_test_schema!
# Model specs: type: :model
# Controller specs: type: :controller
# Request specs: type: :request
# Feature specs: type: :feature
# View specs: type: :view
# Helper specs: type: :helper
# Mailer specs: type: :mailer
# Routing specs: type: :routing
RSpec.configure do |config|
config.order = 'random'
config.seed = srand % 0xFFFF
config.infer_spec_type_from_file_location!
config.use_transactional_fixtures = false
config.infer_base_class_for_anonymous_controllers = false
config.before(:each) { GC.disable }
config.after(:each) { GC.enable }
config.include FactoryGirl::Syntax::Methods
config.include JsonSpec::Helpers
config.include Stubs
config.include LoginHelper
config.include SolrSpecHelper
config.include SunspotMatchers
config.include Devise::TestHelpers, type: :controller
config.before do
Sunspot.session = SunspotMatchers::SunspotSessionSpy.new(Sunspot.session)
end
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
RspecApiDocumentation.configure do |config|
config.format = :json
end
The features/external_product_service_spec.rb
resource 'Retailigence Products and Locations', type: :feature, api: true, slow: true, sunspot: true do
before(:all) do
log_in_as_client!
end
let(:id) { '7c381d47-d251-457a-a2d2-930c8993a5fa' }
let(:lat) { 39.74585 }
let(:long) { -104.998929 }
let(:q) { 'whiteboard cleaner' }
get '/external-products/:id' do
parameter :id, 'The external id to search', required: true
parameter :lat, 'The latitude to search', required: true
parameter :long, 'The longitude to search', required: true
parameter :radius, 'The radius, in miles, to search'
example 'Search a given region for products matching a given external product id' do
do_request user_email: @user.email, user_token: @token, timestamp: @timestamp
expect(response_body).to have_json_type(Array).at_path('external_products')
expect(response_body).to have_json_type(String).at_path('external_products/0/id')
expect(response_body).to have_json_type(String).at_path('external_products/0/name')
expect(response_body).to have_json_type(String).at_path('external_products/0/seo_slug')
expect(response_body).to have_json_type(String).at_path('external_products/0/description')
expect(response_body).to have_json_type(Array).at_path('external_products/0/images')
expect(response_body).to have_json_type(String).at_path('external_products/0/price')
end
end
get '/external-products/:id/external-stores' do
parameter :id, 'The external id to search', required: true
parameter :lat, 'The latitude to search', required: true
parameter :long, 'The longitude to search', required: true
parameter :radius, 'The radius, in miles, to search'
example 'Search a given region for stores which have the product matching a given external product id' do
do_request user_email: @user.email, user_token: @token, timestamp: @timestamp
expect(response_body).to have_json_type(Array).at_path('external_stores')
expect(response_body).to have_json_type(String).at_path('external_stores/0/id')
expect(response_body).to have_json_type(String).at_path('external_stores/0/name')
expect(response_body).to have_json_type(String).at_path('external_stores/0/store_logo')
expect(response_body).to have_json_type(Float).at_path('external_stores/0/longitude')
expect(response_body).to have_json_type(Float).at_path('external_stores/0/latitude')
expect(response_body).to have_json_type(Float).at_path('external_stores/0/distance')
expect(response_body).to have_json_type(String).at_path('external_stores/0/city')
expect(response_body).to have_json_type(String).at_path('external_stores/0/address')
expect(response_body).to have_json_type(String).at_path('external_stores/0/zip')
expect(response_body).to have_json_type(String).at_path('external_stores/0/state')
expect(response_body).to have_json_type(String).at_path('external_stores/0/phone_number')
end
end
get '/external-products/search' do
parameter :q, 'The query string to search', required: true
parameter :lat, 'The latitude to search', required: true
parameter :long, 'The longitude to search', required: true
parameter :radius, 'The radius, in miles, to search'
example 'Search a given region for products by query string' do
do_request q: q, user_email: @user.email, user_token: @token, timestamp: @timestamp
expect(response_body).to have_json_type(Array).at_path('external_products')
expect(response_body).to have_json_type(String).at_path('external_products/0/id')
expect(response_body).to have_json_type(String).at_path('external_products/0/name')
expect(response_body).to have_json_type(String).at_path('external_products/0/seo_slug')
expect(response_body).to have_json_type(String).at_path('external_products/0/description')
expect(response_body).to have_json_type(Array).at_path('external_products/0/images')
expect(response_body).to have_json_type(String).at_path('external_products/0/price')
end
end
get '/external-products/:id/deals' do
before(:each) do
solr_setup
store.location.save
store.location.reload
store.location.index!
end
let(:retailer) { create :retailer }
let!(:deal) { create :deal, retailer_id: retailer.id }
let!(:store) do
create :store, retailer_id: retailer.id,
location: (create :location, latitude: lat, longitude: long)
end
let!(:retailigence_retailer) do
create :retailigence_retailers_retailer, retailer_id: retailer.id,
retailigence_retailer_id: '39bfd9a5-f979-4ef1-816b-f9a38093494a'
end
let!(:content_location) do
create :content_location, store_id: store.id,
locatable_id: deal.id, locatable_type: 'Deal'
end
parameter :id, 'The external id to search', required: true
parameter :lat, 'The latitude to search', required: true
parameter :long, 'The longitude to search', required: true
parameter :radius, 'The radius, in miles, to search'
example 'Search a given region for deals related to a particular product' do
do_request id: id, user_email: @user.email, user_token: @token, timestamp: @timestamp
expect(response_body).to have_json_type(Array).at_path('deals')
expect(response_body).to have_json_type(Integer).at_path('deals/0/id')
expect(response_body).to have_json_type(Integer).at_path('deals/0/retailer_id')
expect(response_body).to have_json_type(String).at_path('deals/0/title')
expect(response_body).to have_json_type(String).at_path('deals/0/seo_slug')
expect(response_body).to have_json_type(String).at_path('deals/0/name')
expect(response_body).to have_json_type(String).at_path('deals/0/sort_name')
expect(response_body).to have_json_type(String).at_path('deals/0/description')
expect(response_body).to have_json_type(:boolean).at_path('deals/0/is_local')
expect(response_body).to have_json_type(:boolean).at_path('deals/0/is_featured')
expect(response_body).to have_json_type(Array).at_path('images')
end
end
get '/external-products/search/deals' do
before(:each) do
solr_setup
store.location.save
store.location.reload
store.location.index!
end
let(:retailer) { create :retailer }
let!(:deal) { create :deal, retailer_id: retailer.id }
let!(:store) do
create :store, retailer_id: retailer.id,
location: (create :location, latitude: lat, longitude: long)
end
let!(:retailigence_retailer) do
create :retailigence_retailers_retailer, retailer_id: retailer.id,
retailigence_retailer_id: 'eea1722b-ac89-4cce-95ec-26c2414646d7'
end
let!(:content_location) do
create :content_location, store_id: store.id,
locatable_id: deal.id, locatable_type: 'Deal'
end
parameter :q, 'The query string to search', required: true
parameter :lat, 'The latitude to search', required: true
parameter :long, 'The longitude to search', required: true
parameter :radius, 'The radius, in miles, to search'
example 'Search a given region for related deals by query string' do
do_request q: q, lat: lat, long: long,
user_email: @user.email, user_token: @token, timestamp: @timestamp
expect(response_body).to have_json_type(Array).at_path('deals')
expect(response_body).to have_json_type(Integer).at_path('deals/0/id')
expect(response_body).to have_json_type(Integer).at_path('deals/0/retailer_id')
expect(response_body).to have_json_type(String).at_path('deals/0/title')
expect(response_body).to have_json_type(String).at_path('deals/0/seo_slug')
expect(response_body).to have_json_type(String).at_path('deals/0/name')
expect(response_body).to have_json_type(String).at_path('deals/0/sort_name')
expect(response_body).to have_json_type(String).at_path('deals/0/description')
expect(response_body).to have_json_type(:boolean).at_path('deals/0/is_local')
expect(response_body).to have_json_type(:boolean).at_path('deals/0/is_featured')
expect(response_body).to have_json_type(Array).at_path('images')
end
end
end
The retailigence_service.rb:
class RetailigenceService
NEGATIVE_KEYWORDS = 'AND !DVD AND !CD AND !"compact disc"'
MAX_QUERY_TIME = 5000
attr_accessor :products, :locations, :params
def self.products(params = {})
fetch(params).products
end
def self.locations(params = {})
fetch_locations(params).locations
end
def self.fetch(params = {})
service = new
service.fetch(params)
service
end
def self.fetch_locations(params = {})
service = new
service.fetch_locations(params)
service
end
def fetch(params = {})
@params = { offset: 0, limit: 25 }.merge(params)
@params.symbolize_keys!
search_result = Retailigence::Product.search(search_params)
@products = search_result.results
build_products
rescue Retailigence::NoResults
@products = []
rescue Retailigence::APIException
@products = []
end
def fetch_locations(params = {})
@params = { offset: 0, limit: 25 }.merge(params)
@params.symbolize_keys!
Rails.logger.debug "[SENDING] #{location_params}"
search_result = Retailigence::Location.search(location_params)
@locations = search_result.results.map { |retailer| retailer.locations }.flatten
build_locations
rescue Retailigence::NoResults
@locations = []
rescue Retailigence::APIException
@locations = []
end
private
def keywords
keywords = @params[:keywords] || ''
keywords << ' ' << NEGATIVE_KEYWORDS
keywords.gsub(/^\ AND\ /, '')
end
def page_size
@params[:limit]
end
def page
(@params[:offset] / @params[:limit]) + 1
end
def location_params
{
userlocation: @params[:userlocation],
requestorid: RETAILIGENCE_REQUESTOR_ID,
productid: @params[:productid],
pagesize: page_size,
page: page,
maxquerytime: MAX_QUERY_TIME,
excludeadultcontent: true
}
end
def search_params
search_params = {
userlocation: @params[:userlocation],
requestorid: RETAILIGENCE_REQUESTOR_ID,
pagesize: page_size, page: page,
maxquerytime: MAX_QUERY_TIME,
excludeadultcontent: true
}
search_params[:keywords] = keywords unless @params[:product_id]
search_params[:productid] = @params[:product_id] if @params[:product_id]
search_params
end
def format_price_prefix(prefix)
prefix = '$' if prefix == 'USD'
prefix
end
def build_products
@products.map! do |p|
p.images ||= []
p.description_long ||= ''
RetailigenceProduct.new(
id: p.id, name: p.name, seo_slug: p.name.slugify,
description: p.description_long,
images: p.images.map { |img| img.link },
price: "#{format_price_prefix p.msrp_currency}#{p.price}",
retailigence_retailer_id: p.location.retailer.id)
end
end
def build_locations
@locations.map! do |l|
RetailigenceLocation.new(
id: l.id, name: l.retailer.name,
latitude: l.latitude, longitude: l.longitude,
phone_number: l.phone, address: l.address.address1,
city: l.address.city, state: l.address.state,
zip: l.address.postal, store_logo: l.retailer.logo,
distance: l.distance.distance)
end
end
end
Here is our Sunspot setup in support/sunspot.rb:
$original_sunspot_session = Sunspot.session
Sunspot.session = Sunspot::Rails::StubSessionProxy.new($original_sunspot_session)
module SolrSpecHelper
def solr_setup
unless $sunspot
$sunspot = Sunspot::Rails::Server.new
pid = fork do
STDERR.reopen('/dev/null')
STDOUT.reopen('/dev/null')
$sunspot.run
end
# shut down the Solr server
at_exit { Process.kill('TERM', pid) }
# wait for solr to start
sleep 5
end
Sunspot.session = $original_sunspot_session
end
end