I was tempted to use rswag with rspec to document REST API and write test at the same time.
I am trying to fallow tutorials and documentations but I cannot get sign_in endpoint working ( devise - session ).
When I do run rspec than I receive status code error.
require 'swagger_helper'
require 'rails_helper'
require 'shared_context'
describe 'Sonaaar REST API', type: :request do
...
path '/users/sign_in' do
post 'Sign In' do
tags 'Session'
consumes 'application/json'
produces 'application/json'
parameter name: :user, in: :body, schema: {
type: :object,
properties: {
email: { type: :string },
password: { type: :string },
},
required: ['email', 'password']
}
response '201', 'sign in', { 'HTTP_ACCEPT' => "application/json" } do
response '201', 'sign in', { 'HTTP_ACCEPT' => "application/json" } do
#
# let(:user) do
# create(:user, email: 'email#domain.com', password: 'Password1')
# end
#Error
# Failure/Error:
# raise UnexpectedResponse,
# "Expected response code '#{response.code}' to match '#{expected}'\n" \
# "Response body: #{response.body}"
#
# Rswag::Specs::UnexpectedResponse:
# Expected response code '401' to match '201'
# Response body: {"error":"You need to sign in or sign up before continuing."}
let(:user) { { user: { login: 'email#domain.com', password: 'Password1' } } }
# Rswag::Specs::UnexpectedResponse:
# Expected response code '401' to match '201'
# Response body: {"error":"You need to sign in or sign up before continuing."}
# /Users/filip/.rvm/gems/ruby-2.7.1/gems/rswag-specs-2.4.0/lib/rswag/specs/r
run_test!
end
Than i do have RSpec error:
1) Sonaaar REST API /users/sign_in post sign in returns a 201 response
Failure/Error:
raise UnexpectedResponse,
"Expected response code '#{response.code}' to match '#{expected}'\n" \
"Response body: #{response.body}"
Rswag::Specs::UnexpectedResponse:
Expected response code '401' to match '201'
Response body: {"error":"You need to sign in or sign up before continuing."}
Authentication: JWT-token
Content Type: application/json
Stack/Gems:
Ruby on Rails (Rails 6.1.2.1)
devise (4.7.3)
devise-jwt (0.7.0)
rspec-rails (4.0.2)
rswag (2.4.0) - https://github.com/rswag/rswag
Request specs need a little bit more help than just the normal warden helpers that work for feature specs:
RSpec.configure do |config|
config.include Warden::Test::Helpers
end
Add to rails_helper or another file and require in rails_helper:
module DeviseRequestSpecHelpers
include Warden::Test::Helpers
def sign_in(user)
login_as(user, scope: :user)
end
def sign_out
logout(:user)
end
end
Then in rails_helper add
RSpec.configure do |config|
config.include DeviseRequestSpecHelpers, type: :request
end
You should be able to login now in request specs with:
context 'some context' do
scenario 'some scenario' do
login_as(user)
end
end
as #sam said, but actually you can just include the warden helpers
RSpec.configure do |config|
config.include Warden::Test::Helpers
end
And then
let(:admin) { create(:admin) } # for this case it's devise :admins
before do
login_as(admin, scope: :admin)
end
# If you want to logout admin
after do
logout(:admin)
end
it 'should return 200 'do
get 'some_path_that_needs_session'
expect(response).to have_http_status(200)
end
You can refer to [Warden::Test::Helpers] (https://www.rubydoc.info/github/hassox/warden/Warden/Test/Helpers) docs
Related
I am trying to get devise and devise-jwt gems to work so I can implement Authorization into my API only Rails app.
I have installed both devise and devise-jwt gems.
I followed the instructions on this blog post:
https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03
I have implemented the request specs that the author has included in his post and I can't get it approved on "Deleted",
I have to pass the authorizate token on delete, but I'm not getting it.
Any suggestion?
require 'rails_helper'
require "json"
RSpec.describe "POST /users", type: :request do
let(:user) { create(:user) }
let(:url) { '/users/sign_in' }
let(:params) do
{
user: {
email: user.email,
password: user.password
}
}
end
context 'when params are correct' do
before do
post url, params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
it 'returns 200' do
expect(response).to have_http_status(200)
end
it 'returns JTW token in authorization header' do
expect(response.headers['authorization']).to be_present
end
end
context 'when login params are incorrect' do
before { post url }
it 'returns unathorized status' do
expect(response.status).to eq 401
end
end
end
RSpec.describe 'DELETE /logout', type: :request do
let(:url) { '/users/sign_out' }
it 'returns 204, no content' do
delete url
expect(response).to have_http_status(201)
end
end
I need to pass the user's token on delete, any suggestions on how I can be doing this?
I am trying to get devise and devise-jwt gems to work so I can implement Authorization into my API only Rails app.
I have installed both devise and devise-jwt gems.
I followed the instructions on this blog post:
https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03
I implemented the requests specs the author included in his post, and I can't get them to pass. If I put a byebug into the session controller, I see that it's saying the "User needs to sign in or sign up before continuing."
Any thoughts on what I'm doing incorrectly?
Here are the relevant files:
routes.rb
Rails.application.routes.draw do
namespace :api, path: '', defaults: {format: :json} do
namespace :v1 do
devise_for :users,
path: '',
path_names: {
sign_in: 'signin',
sign_out: 'signout',
registration: 'signup'
}
...
end
end
controllers/api/v1/sessions_controller.rb
class API::V1::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: resource
end
def respond_to_on_destroy
head :no_content
end
end
models/user.rb
class User < ApplicationRecord
devise :confirmable, :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: JwtBlacklist
...
end
models/jwt_blacklist.rb
class JwtBlacklist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Blacklist
self.table_name = 'jwt_blacklist'
end
config/initializers/devise.rb
Devise.setup do |config|
# Setup for devise JWT token authentication
config.jwt do |jwt|
jwt.secret = Rails.application.secret_key_base
jwt.dispatch_requests = [
['POST', %r{^*/signin$}]
]
jwt.revocation_requests = [
['DELETE', %r{^*/signout$}]
]
jwt.expiration_time = 1.day.to_i
end
config.navigational_formats = []
...
end
spec/request/authentication_spec.rb
require 'rails_helper'
describe 'POST /v1/signin', type: :request do
let(:user) { create(:user) }
let(:url) { '/v1/signin' }
let(:params) do
{
user: {
email: user.email,
password: user.password
}
}
end
context 'when params are correct' do
before do
post url, params: params
end
it 'returns 200' do
expect(response).to have_http_status(200)
end
it 'returns JTW token in authorization header' do
expect(response.headers['Authorization']).to be_present
end
it 'returns valid JWT token' do
decoded_token = decoded_jwt_token_from_response(response)
expect(decoded_token.first['sub']).to be_present
end
end
context 'when login params are incorrect' do
before { post url }
it 'returns unathorized status' do
expect(response.status).to eq 401
end
end
end
describe 'DELETE /v1/signout', type: :request do
let(:url) { '/v1/signout' }
it 'returns 204, no content' do
delete url
expect(response).to have_http_status(204)
end
end
I would expect the tests to pass, but I get the following errors:
Test Failures
Failures:
1) POST /v1/signin when params are correct returns 200
Failure/Error: expect(response).to have_http_status(200)
expected the response to have status code 200 but it was 401
# ./spec/request/authentication_spec.rb:21:in `block (3 levels) in <top (required)>'
2) POST /v1/signin when params are correct returns JTW token in authorization header
Failure/Error: expect(response.headers['Authorization']).to be_present
expected `nil.present?` to return true, got false
# ./spec/request/authentication_spec.rb:25:in `block (3 levels) in <top (required)>'
3) POST /v1/signin when params are correct returns valid JWT token
Failure/Error: decoded_token = decoded_jwt_token_from_response(response)
NoMethodError:
undefined method `decoded_jwt_token_from_response' for #<RSpec::ExampleGroups::POSTV1Signin::WhenParamsAreCorrect:0x00007fec3d3ae158>
# ./spec/request/authentication_spec.rb:29:in `block (3 levels) in <top (required)>'
Finished in 0.76386 seconds (files took 3.31 seconds to load)
5 examples, 3 failures
Failed examples:
rspec ./spec/request/authentication_spec.rb:20 # POST /v1/signin when params are correct returns 200
rspec ./spec/request/authentication_spec.rb:24 # POST /v1/signin when params are correct returns JTW token in authorization header
rspec ./spec/request/authentication_spec.rb:28 # POST /v1/signin when params are correct returns valid JWT token
I don't know if you found a solution; but I leave an approach I've made; It might helpfull.
Taking special attetion to the problem, The solution was to change:
decoded_token = decoded_jwt_token_from_response(response)
To:
decoded_token = JWT.decode(response.headers['authorization'].split(' ').last, Rails.application.credentials.jwt_secret, true)
Beacuse I din't find any in the documentation or other place and I chose to decode with method provided by JWT.
Also if you see I handle the requests in a different way, but I think that is not a problem at all.
require 'rails_helper'
require "json"
RSpec.describe "POST /login", type: :request do
let(:user) { User.create!( username: 'usertest',
email: 'usertest#email.com',
password: 'passwordtest123',
password_confirmation: 'passwordtest123') }
let(:url) { '/users/login' }
let(:params) do
{
user: {
login: user.email,
password: user.password
}
}
end
context 'when params are correct' do
before do
post url, params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
it 'returns 200' do
expect(response).to have_http_status(200)
end
it 'returns JTW token in authorization header' do
expect(response.headers['authorization']).to be_present
end
it 'returns valid JWT token' do
token_from_request = response.headers['Authorization'].split(' ').last
decoded_token = JWT.decode(token_from_request, Rails.application.credentials.jwt_secret, true)
expect(decoded_token.first['sub']).to be_present
end
end
context 'when login params are incorrect' do
before { post url }
it 'returns unathorized status' do
expect(response.status).to eq 401
end
end
end
RSpec.describe 'DELETE /logout', type: :request do
let(:url) { '/users/logout' }
it 'returns 204, no content' do
delete url
expect(response).to have_http_status(204)
end
end
RSpec.describe 'POST /signup', type: :request do
let(:url) { '/users/signup' }
let(:params) do
{
user: {
username: 'usertest2',
email: 'usertest2#email.com',
password: 'passwordtest123',
password_confirmation: 'passwordtest123'
}
}
end
context 'when user is unauthenticated' do
before {
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
}
it 'returns 201' do
expect(response.status).to eq 201
end
it 'returns a new user' do
expect(response).to have_http_status :created
end
end
context 'when user already exists' do
before do
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
it 'returns bad request status' do
expect(response.status).to eq 400
end
it 'returns validation errors' do
expect(response_body['errors'].first['title']).to eq('Bad Request')
end
end
end
PD: I leave the spec code for register, with a couple differences (requests, url, username params in User model (that's is why I use the login param y the login request), I made all in a sigle spec.rb file, ...) to https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03. Kepp that in mind.
I believe you need to use the helper sign_in user before making the request for it to be authorized. Check https://github.com/heartcombo/devise, Controller tests
I have a Rails 5 API only app and using knock to do JWT authenticate.
After complete the model and model spec, I start to do the request spec.
But I have no idea how to complete the authentication inside the request spec in the right way,
My users controller,
module V1
class UsersController < ApplicationController
before_action :authenticate_user, except: [:create]
end
end
Application controller,
class ApplicationController < ActionController::API
include Knock::Authenticable
include ActionController::Serialization
end
My stupidest solution (call the get token request to get the JWT before the rest request),
context 'when the request contains an authentication header' do
it 'should return the user info' do
user = create(:user)
post '/user_token', params: {"auth": {"email": user.email, "password": user.password }}
body = response.body
puts body # {"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0ODgxMDgxMDYsInN1YiI6MX0.GDBHPzbivclJfwSTswXhDkV0TCFCybJFDrjBnLIfN3Q"}
# use the retrieved JWT for future requests
end
end
Any advice is appreciated.
def authenticated_header(user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
{ 'Authorization': "Bearer #{token}" }
end
describe 'GET /users?me=true' do
URL = '/v1/users?me=true'
AUTH_URL = '/user_token'
context 'when the request with NO authentication header' do
it 'should return unauth for retrieve current user info before login' do
get URL
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the request contains an authentication header' do
it 'should return the user info' do
user = create(:user)
get URL, headers: authenticated_header(user)
puts response.body
end
end
end
With the help of Lorem's answer, I was able to implement something similar for my request spec. Sharing it here for others to see an alternate implementation.
# spec/requests/locations_spec.rb
require 'rails_helper'
RSpec.describe 'Locations API' do
let!(:user) { create(:user) }
let!(:locations) { create_list(:location, 10, user_id: user.id) }
describe 'GET /locations' do
it 'reponds with invalid request without JWT' do
get '/locations'
expect(response).to have_http_status(401)
expect(response.body).to match(/Invalid Request/)
end
it 'responds with JSON with JWT' do
jwt = confirm_and_login_user(user)
get '/locations', headers: { "Authorization" => "Bearer #{jwt}" }
expect(response).to have_http_status(200)
expect(json.size).to eq(10)
end
end
end
confirm_and_login_user(user) is defined in a request_spec_helper which is included as a module in rails_helper.rb:
# spec/support/request_spec_helper.rb
module RequestSpecHelper
def json
JSON.parse(response.body)
end
def confirm_and_login_user(user)
get '/users/confirm', params: {token: user.confirmation_token}
post '/users/login', params: {email: user.email, password: 'password'}
return json['auth_token']
end
end
I'm using the jwt gem for generating my tokens as described in this SitePoint tutorial (https://www.sitepoint.com/introduction-to-using-jwt-in-rails/)
Lorem's answer mostly worked for me. I got unrecognized keyword setting headers: on the get. I modified the authenticated_header method and put it in support/api_helper.rb so I could reuse it. The modification is to merge the auth token into request.headers.
# spec/support/api_helper.rb
module ApiHelper
def authenticated_header(request, user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
request.headers.merge!('Authorization': "Bearer #{token}")
end
end
In each spec file testing the api, I include api_helper.rb. And I call authenticated_header just before the get statement when testing the case of valid authentication...
# spec/controllers/api/v2/search_controller_spec.rb
RSpec.describe API::V2::SearchController, type: :controller do
include ApiHelper
...
describe '#search_by_id' do
context 'with an unauthenticated user' do
it 'returns unauthorized' do
get :search_by_id, params: { "id" : "123" }
expect(response).to be_unauthorized
end
end
context 'with an authenticated user' do
let(:user) { create(:user) }
it 'renders json listing resource with id' do
expected_result = { id: 123, title: 'Resource 123' }
authenticated_header(request, user)
get :search_by_id, params: { "id" : "123" }
expect(response).to be_successful
expect(JSON.parse(response.body)).to eq expected_result
end
end
The key lines in this second test are...
authenticated_header(request, user)
get :search_by_id, params: { "id" : "123" }
Devise: 4.20
Rails: 5.0.1
Rspec: 3.5
I had used this link https://github.com/plataformatec/devise/wiki/How-To:-Use-HTTP-Basic-Authentication, but I havproblems to test the http basic auth for my api using rspec for requests tests. Below is the example the error:
app/controllers/api/base_controller.rb
module Api
class BaseController < ApplicationController
before_action :authenticate_user!
protected
def authenticate_user!
authenticate_or_request_with_http_basic do |username, password|
resource = User.find_by_username(username)
if resource
sign_in :user, resource if resource.valid_password?(password)
else
request_http_basic_authentication
end
end
end
end
end
app/controllers/api/v1/car_controller.rb
module Api
module V1
class CarController < Api::BaseController
respond_to :json
def index
#cars = Car.all
render :json => {:content => #cars}, :status => 200
end
end
end
end
spec/requests/api/v1/car_controller_spec.rb
require 'rails_helper'
RSpec.describe "Servers API", :type => :request do
it 'sends a list of servers' do
admin = FactoryGirl.create(:admin)
#env = {}
#env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(admin.username, admin.password)
get "/api/cars", :params => {}, :headers => #env
# test for the 200 status-code
expect(response.status).to eq(200)
end
end
When I run the spec, I have the below error:
# --- Caused by: ---
# NoMethodError:
# undefined method `sign_in' for #<Api::V1::CarController:0x0000000609ef12>
# ./app/controllers/api/base_controller.rb:10 in block in authenticate_user!
Anyone can help me? Thanks.
I have similar setup where my specs are passing, would you also show your spec_helper content, looks like you are not including Devise::TestHelpers.
spec_helper
RSpec.configure do |config|
config.include Devise::TestHelpers
config.include Warden::Test::Helpers
config.before { Warden.test_mode! }
config.after { Warden.test_reset! }
config.before(:each) do
#headers = { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
end
and my test looks something like this:
RSpec.describe 'Users' do
context 'when client is authorized' do
let(:user) { FactoryGirl.create(:user) }
it 'gets user' do
#headers['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.
encode_credentials(
user.authentication_token,
email: user.email
)
get api_v1_user_url(id: user.id), {}, #headers
expect(response.status).to eq(200)
end
end
end
I'm using Auth0 for authentication in my rails app. I need to write some feature tests for login and signup. I can't seem to find something concrete on how to do this with rspec and capybara.
Tried doing something along the lines explained in this gist but it still doesn't work. If someone has had experience with rspec feature tests with Auth0 I'd appreciate if you would guide me in the right direction.
Thanks!
My configuration
# in spec/support/omniauth_macros.rb
module OmniauthMacros
def mock_auth_hash
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[:auth0] = {
'provider' => 'auth0',
'uid' => '123545',
'user_info' => {
'name' => 'mockuser',
'image' => 'mock_user_thumbnail_url'
},
'credentials' => {
'token' => 'mock_token',
'secret' => 'mock_secret'
}
}
end
end
# in spec/requests/spec_helper.rb
RSpec.configure do |config|
# ...
# include our macro
config.include(OmniauthMacros)
end
OmniAuth.config.test_mode = true
Then in my spec I have
scenario 'Should successfully login user' do
visit login_path
mock_auth_hash
click_link "Sign in"
expect(page).to have_content('Signed in successfully')
expect(page).to have_link("Logout", href: logout_path)
end
This is how I solved it
# in spec/support/omniauth_macros.rb
module OmniauthMacros
def mock_valid_auth_hash(user)
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
opts = {
"provider": user.provider,
"uid": user.uid,
"info": {
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
},
"credentials": {
"token": "XKLjnkKJj7hkHKJkk",
"expires": true,
"id_token": "eyJ0eXAiOiJK1VveHkwaTFBNXdTek41dXAiL.Wz8bwniRJLQ4Fqx_omnGDCX1vrhHjzw",
"token_type": "Bearer"
}
}
OmniAuth.config.mock_auth[:auth0] = OmniAuth::AuthHash.new(opts)
end
def mock_invalid_auth_hash
OmniAuth.config.mock_auth[:auth0] = :invalid_credentials
end
end
# in spec/support/features/session_helpers.rb
module Features
module SessionHelpers
def log_in(user, invalid=false, strategy = :auth0)
invalid ? mock_invalid_auth_hash : mock_valid_auth_hash(user)
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[strategy.to_sym]
visit "/auth/#{strategy.to_s}/callback?code=vihipkGaumc5IVgs"
end
end
end
# in spec/support/rails_helper.rb
RSpec.configure do |config|
# ...
# include our macro
config.include(OmniauthMacros)
# include Session helpers
config.include Features::SessionHelpers, type: :feature
end
And finally my feature test to simulate login looks like
# user signin feature test
require 'rails_helper'
feature 'Login with Omniauth' do
given(:user) { create(:user) }
context "With valid credentials" do
scenario 'Should successfully login user' do
log_in(user)
expect(page).to have_content("successfully signed in")
expect(page).to have_link("Logout", href: logout_path)
end
end
context "With invalid credentials" do
scenario "Should not log in user" do
log_in(user, true)
expect(page).to have_content('invalid_credentials')
end
end
end
Hope this helps!