I am using devise_auth_token to authenticate users for an API. I would like to authenticate users before each test is run, but keep getting a 401 error. When I use postman to the endpoint with the correct headers, it works, but fails to work during tests.
before(:each) do
#user = FactoryGirl.create(:user)
end
def get_auth
headers = #user.create_new_auth_token
auth = Hash.new
auth["client"] = headers["client"]
auth["access-token"] = headers["access-token"]
auth["uid"] = headers["uid"]
auth["expiry"] = headers["expiry"]
return auth
end
it "auth user should return success" do
get 'get_tasks_for_user', params: {uid: #user.uid}, headers: get_auth
expect(response).to have_http_status(200)
end
RSpec
TasksController auth user should return success
Failure/Error: expect(response).to have_http_status 200
expected the response to have status code 200 but it was 401
You can use a helper method
#/spec/support/helpers/session_helper.rb
module SessionHelper
def set_request_headers(resp)
{ 'ACCEPT' => "application/json",
'Content-Type' => "application/json",
'access-token' => resp['access-token'],
'token-type' => resp['token-type'],
'client' => resp['client'],
'expiry' => resp['expiry'],
'uid' => resp['uid']
}
end
def subdomain_login(uid, password, subdomain)
request_params = {
'email' => uid,
'password' => password
}
host! "#{subdomain}.lvh.me"
post "/portal/auth/sign_in", params: request_params
return set_request_headers(response.headers)
end
end
Make sure you have the following entry in your /spec/rails_helper
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
config.include SessionHelper, :type => :request
end
You can use subdomain_login in your tests. Here is an Rspec example.
post '/portal/system/locations', params: request_params.to_json,
headers: subdomain_login(user_id, password, subdomain)
Related
I am trying to figure out why I can't use use both params and headers in an request spec.
What works:
RSpec.describe Api::V1::UsersController, :type => :request do
before { host! 'api.localhost:3000'}
let(:params) {
{
"user": {
"identifier_for_vendor": "BD43813E"
}
}
}
describe 'Post /users' do
context 'when request is valid' do
before {
post api_users_path,
params: params
}
it "is successful" do
expect(response).to be_successful
end
end
end
end
What does not:
RSpec.describe Api::V1::UsersController, :type => :request do
let(:params) {
{
"user": {
"identifier_for_vendor": "BD43813E"
}
}
}
let(:headers) {
{
"host": "api.localhost:3000",
"Accept": "application/vnd.domain_name.v1",
"Content-Type": "application/vnd.api+json",
"X-API-Key": "fake087uakey"
}
}
describe 'Post /users' do
context 'when request is valid' do
before {
post api_users_path,
params: params,
headers: headers
}
it "successful" do
expect(response).to be_successful
end
end
end
end
The above fails, returning the error:
1) Api::V1::UsersController Post /users when request is valid is successful
Failure/Error: params.require(:user).permit(:identifier_for_vendor)
ActionController::ParameterMissing:
param is missing or the value is empty: user
The headers are needed due to having to ensure that valid api-keys are included in the request.
Would appreciate feedback on what I am missing. Thank you
Versions:
Ruby version: 2.6.3
Rails version: 6.0.3.4
RSpec 3.10
So issue had to do with how params and headers objects are created.
Params:
I passed in:
{"user": {"identifier_for_vendor": "OFJPJ"} }
the correct object is:
{:params=>{:user=>{:identifier_for_vendor=>"OFJPJ"}}}
Headers:
I passed in:
{
"host": "api.localhost:3000",
"Accept": "application/vnd.domain_name.v1",
"Content-Type": "application/vnd.api+json",
"X-API-Key": "fake087uakey"
}
the correct object is:
{
"headers" => {
"host" => "api.localhost:3000",
"Accept" => "application/vnd.domain_name.v1",
"X-API-Key" => "api_key"
}
}
Final solution looks like this:
RSpec.describe Api::V1::UsersController, :type => :request do
describe 'Post /users' do
context 'when request is valid' do
before do
post api_users_path,
:params => params,
:headers => headers
end
it "is successful" do
expect(response).to be_successful
end
it "returns a data of type user" do
expect(json_data["type"]).to eq("user")
end
end
end
end
The key to figuring this out was reading the documentation and realizing the my formatting was wrong.
In my Rails/Grape app I created a webhook controller which receive JSON from CMS webhook. I'm just wondering how to test it in RSpec if I don't have any params (I guess I don't need it because I only receive JSON from webhook).
My webhook controller (it works well):
module Cms
class Webhook < Base
desc 'Take the CMS webhook'
http_basic do |user, password|
user == ENV['USER'] && password == ENV['PASSWORD']
end
post :receive do
params
end
end
end
I was trying to like:
describe Cms::Webhooks, type: :request do
subject(:call) { post endpoint, params: params, as: :json }
let(:endpoint) { '/api/cms/webhooks/receive' }
let(:params) do
{
some: 'some pass'
}
end
it 'returns a successful response' do
call
expect(response).to be_successful
end
end
I'm getting an error:
Failure/Error: expect(response).to be_successful
expected `#<ActionDispatch::TestResponse:0x00007f9058e43e60 #mon_data=#<Monitor:0x00007f9058e43de8>, #mon_data_..., #method=nil, #request_method=nil, #remote_ip=nil, #original_fullpath=nil, #fullpath=nil, #ip=nil>>.successful?` to return true, got false
Can you try this code?
describe Cms::Webhooks, type: :request do
subject(:call) { post endpoint, params: params, as: :son, headers: headers }
let(:endpoint) { '/api/cms/webhooks/receive' }
let(:params) do
{
some: 'some pass'
}
end
let(:headers) do
{
'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('your_username', 'your_password')
}
end
it 'returns a successful response' do
call
expect(response).to be_successful
end
end
Ref https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
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.
I'm trying to test CredentialsController, which works fine in production, using RSpec request specs.
Code
Controller
class CredentialsController < ApplicationController
before_action :doorkeeper_authorize!
def me
render json: current_user
end
end
(GET /me routes to CredentialsController#me.)
Request Specs
describe 'Credentials', type: :request do
context 'unauthorized' do
it "should 401" do
get '/me'
expect(response).to have_http_status(:unauthorized)
end
end
context 'authorized' do
let!(:application) { FactoryBot.create(:application) }
let!(:user) { FactoryBot.create(:user) }
let!(:token) { FactoryBot.create(:access_token, application: application, resource_owner_id: user.id) }
it 'succeeds' do
get '/me', params: {}, headers: {access_token: token.token}
expect(response).to be_successful
end
end
end
The unauthorized test passes, but the authorized test fails:
expected #<ActionDispatch::TestResponse:0x00007fd339411248 #mon_mutex=#<Thread::Mutex:0x00007fd339410438>, #mo..., #method=nil, #request_method=nil, #remote_ip=nil, #original_fullpath=nil, #fullpath=nil, #ip=nil>>.successful? to return true, got false
The headers indicate a problem with the token:
0> response.headers['WWW-Authenticate']
=> "Bearer realm=\"Doorkeeper\", error=\"invalid_token\", error_description=\"The access token is invalid\""
token looks okay to me, though:
0> token
=> #<Doorkeeper::AccessToken id: 7, resource_owner_id: 8, application_id: 7, token: "mnJh2wJeEEDe0G-ukNIZ6oupKQ7StxJqKPssjZTWeAk", refresh_token: nil, expires_in: 7200, revoked_at: nil, created_at: "2020-03-19 20:17:26", scopes: "public", previous_refresh_token: "">
0> token.acceptable?(Doorkeeper.config.default_scopes)
=> true
Factories
Access Token
FactoryBot.define do
factory :access_token, class: "Doorkeeper::AccessToken" do
application
expires_in { 2.hours }
scopes { "public" }
end
end
Application
FactoryBot.define do
factory :application, class: "Doorkeeper::Application" do
sequence(:name) { |n| "Project #{n}" }
sequence(:redirect_uri) { |n| "https://example#{n}.com" }
end
end
User
FactoryBot.define do
factory :user do
sequence(:email) { |n| "email#{n}#example.com" }
password { "test123" }
password_confirmation { "test123" }
end
end
Questions
Why am I getting invalid_token on this request?
Do my Doorkeeper factories look correct?
I was passing the token wrong. Instead of:
get '/me', params: {}, headers: {access_token: token.token}
I had to use:
get '/me', params: {}, headers: { 'Authorization': 'Bearer ' + token.token}
You can check your Access Token factory's scopes, It should be same as initializer's default_scopes
e.g.
config/initializers/doorkeeper.rb
default_scopes :read
Below, your Access Token factory's scopes should be
factory :access_token, class: "Doorkeeper::AccessToken" do
sequence(:resource_owner_id) { |n| n }
application
expires_in { 2.hours }
scopes { "read" }
end
Additionally, if you encountered response status: 406 while get '/me'....
It means that the requested format (by default HTML) is not supported. Instead of '.json' you can also send Accept="application/json" in the HTTP header.
get '/me', params: {}, headers: {
'Authorization': 'Bearer ' + token.token,
'Accept': 'application/json'}
I resolved my problem with this solution, maybe you can try it.
I have two RSpec tests, a controller spec and a request spec, where I am making a GET request to the index action of the same controller. In both specs I am sending an Authorization header that contains an Oauth2 bearer token.
The problem I'm having is that depending on the type of spec, the header is stored on a different property of the request object. In the case of the request spec, it is available at request.env["Authorization"] and in the case of the controller spec, it is available at request.session["Authorization"].
Why is "Authorization" stored in different places for different types of specs? Is there somewhere I can find it for both specs?
This bearer_token method is in the parent controller class where I'm grabbing the token from the header:
Works with env in the request specs:
def bearer_token
pattern = /^Bearer /
header = request.env["Authorization"] # <= env
header.gsub(pattern, '') if header && header.match(pattern)
end
Works with session in the controller specs:
def bearer_token
pattern = /^Bearer /
header = request.session["Authorization"] # <= session
header.gsub(pattern, '') if header && header.match(pattern)
end
Here is my request spec:
describe '' do
let(:user) { Fabricate(:user) }
describe 'accessing content with valid token' do
let(:token) { OauthToken.create(user: user) }
let(:auth_headers) { {
'Authorization' => "Bearer #{token.access_token}",
'HTTPS' => 'on'
} }
before { get api_v2_cats_path, {}, auth_headers }
specify { response.status.should == 200 }
end
end
Here is my controller spec
describe Api::V2::CatsController do
let(:user) { Fabricate(:user) }
describe ".index" do
let(:token) { OauthToken.create(user: user) }
let(:auth_headers) { {
'Authorization' => "Bearer #{token.access_token}",
'HTTPS' => 'on'
} }
it "should be valid" do
get :index, { format: :json, page_size: 1 }, auth_headers
#json = JSON.parse(response.body)
#json.should_not be_nil
end
end
end
I assumed that the API would be the same for the get method between a request and controller spec. In the controller spec, the third argument is a hash of sessions variables, not header variables. You can set the headers directly on the #request object like so:
describe Api::V2::CatsController do
let(:user) { Fabricate(:user) }
describe ".index" do
let(:token) { OauthToken.create(user: user) }
let(:auth_headers) { {
'Authorization' => "Bearer #{token.access_token}",
'HTTPS' => 'on'
} }
before do
#request.env.merge!(auth_headers)
end
it "should be valid" do
get :index, { format: :json, page_size: 1 }
#json = JSON.parse(response.body)
#json.should_not be_nil
end
end
end
Then the correct way to get the authorization header is using:
def bearer_token
pattern = /^Bearer /
header = request.env["Authorization"] # <= env
header.gsub(pattern, '') if header && header.match(pattern)
end
I found this.
https://github.com/rails/rails/commit/cf9d6a95e805bdddfa9c6b541631d51b3165bf23#diff-10b31f2069dfc4810229c8d60c3a4cda
in your controller, you can do something like this to get the header value.
def index
header_value = request.authorization
end