I have a basic Rails API built with Accounts and Users. All of the account specs pass when I remove...
before_action :authenticate_user!
But with that in place, I'm having trouble getting the specs to pass.
# Note `json` is a custom helper to parse JSON responses
RSpec.describe 'Account API', type: :request do
# test data
let!(:user) { create(:user) }
let!(:accounts) { create_list(:account, 10, user_id: user.id) }
let(:account_id) { accounts.first.id }
# GET /accounts
describe 'GET /accounts' do
# HTTP request before examples
before do
get '/accounts'
request.headers.merge! user.create_new_auth_token
end
it 'returns accounts' do
expect(json).not_to be_empty
expect(json.size).to eq(10)
end
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
end
end
As you can see I attempted to merge the auth token with this line in a before do block...
request.headers.merge! user.create_new_auth_token
But that is not working. Instead I get json.size == 1 and http_status of 401 unauthorized.
I was able to fix it by declaring a variable at the top...
let(:auth_headers) { create(:user).create_new_auth_token }
And in my specs using it like this...
before { get '/accounts', headers: auth_headers }
Related
I have a request spec and I wanted to test CRUD.
The problem is whatever request I put first succeeds, but the rest that follows fail. (Meaning, in the case below, GET will succeed, but POST will fail. If
I switch the 2 context blocks, POST then succeeds, then GET fails).
The failing test says the response is 401 Unauthorized. I'm not sure if the api_key suddenly becomes invalid or something. Or it has something to do with the role I assigned it. (I assigned a system_admin role to the user to be able to CRUD via cancancan)
The way that makes it all work is if I put all requests in one big it block (which I think is not good practice since it returns only 1 passing example, when in reality I have more than 1.)
I've also tried signing in my user again in each block but still the same thing happens.
I am very new to RSpec so any help would be appreciated. Thank you very much!
require 'rails_helper'
RSpec.describe "Departments", type: :request do
user = FactoryBot.build(:user)
role = FactoryBot.build(:role, name: "system_admin")
user.add_role(role.name)
headers = { "x-app-api-key" => user.api_key }
before do
sign_in user
end
describe "CRUD /v1/departments" do
context "GET" do
it "called get" do
get api_departments_path, headers: headers
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(response).to have_http_status(:success)
end
end
context "POST" do
it "called post" do
post api_departments_path, headers: headers, params: { name: "department" }
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(response).to have_http_status(:success)
end
end
end
end
There are a LOT of problems with this spec. First off the bat is your use of local variables instead of let/let!:
require 'rails_helper'
RSpec.describe "Departments", type: :request do
user = FactoryBot.build(:user)
role = FactoryBot.build(:role, name: "system_admin")
user.add_role(role.name)
The problem with this is that these variables will be set when RSpec sets up the spec and not for each example. So provided you actually did insert the records into the database they will still be wiped after the first example is run and all the subsequent examples will fail.
So lets fix that problem:
require 'rails_helper'
RSpec.describe "Departments", type: :request do
let(:user) { FactoryBot.create(:user) }
let(:role) { FactoryBot.create(:role, name: "system_admin") }
before do
user.add_role(role.name)
end
let is evaluated once per example and then memoized.
Note the use of create instead of build which saves the record in the database so that it can actually be accessed from the controller.
Then there is the problem that these tests don't actually provide any value or describe the behavior of the application that you're testing besides testing the http response code.
Test your application - not your tests. If the failure message doesn't tell you anything about what part of the application doesn't work the test is near worthless.
require 'rails_helper'
RSpec.describe "Departments", type: :request do
let(:user) { FactoryBot.create(:user) }
let(:role) { FactoryBot.create(:role, name: "system_admin") }
let(:headers) do
{ "x-app-api-key" => user.api_key }
end
let(:json) { response.parsed_body }
before do
user.add_role(role.name)
sign_in user
end
describe "GET /v1/departments" do
let(:departments) { FactoryBot.create_list(:department, 5) }
it "responds with a list of the departments" do
get api_departments_path, headers: headers
expect(response).to be_successful
# #todo ensure the JSON response contains the expected departments
end
end
describe "POST /v1/departments" do
context "with invalid parameters" do
let(:params) { { name: "" } } # this should just be an invalid or incomplete set of attributes
it "doesn't create a department" do
expect do
post api_departments_path, headers: headers, params: params
end.to_not change(Department, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
end
context "with valid parameters" do
let(:params) { FactoryBot.attributes_for(:department) }
it "creates a department" do
expect do
post api_departments_path, headers: headers, params: params
end.to change(Department, :count).by(1)
expect(response).to have_http_status(:created)
# #todo test that either a JSON representation is returned
# or that the response contains a location header with the URI
# to the resource
end
end
end
end
Testing the content type is largely superflous as you set the content type in the defaults for an API app and testing the JSON responses would fail if its not returning application/json.
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 new to RSpec but here I am trying to create tests based on this code and I am keep on getting this error. Any suggestions?
CODE:
serialization_scope nil
before_action :set_list, only: [:show, :destroy, :update]
before_action :verify_user, only: :show
def create
#list = current_user.lists.build(list_params)
if #list.save
render json: {message: ['Success']}, status: 200
else
render json: {errors:[#list.errors.full_messages]}, status: 400
end
end
Here is the RSpec file that I started :
require "rails_helper"
RSpec.describe V1::ListsController, :type => :controller do
describe "POST create" do
it "returns HTTP status" do
expect(post :create).to change(#list, :count).by(+1)
expect(response).to have_http_status :success #200
end
end
describe 'GET status if its not created' do
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
expect(response.status).to eq 400
end
end
end
And the error that I got is :
Failures:
1) V1::ListsController GET status if its created returns HTTP status
Failure/Error: expect(post :create).to change(#list, :count).by(+1)
expected #count to have changed by 1, but was not given a block
# ./spec/controllers/lists_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
2) GET status if its not created return HTTP status - reports BAD REQUEST (HTTP status 400)
Failure/Error: expect(response.status).to eq 400
expected: 400
got: 200
(compared using ==)
Try this code.
require 'rails_helper'
RSpec.describe V1::ListsController, type: :request do
describe 'valid request' do
it 'returns HTTP status' do
post '/list', params: { list: { list_name: 'xyz' } }
expect(response.status).to eq 201
end
end
describe 'invalid request' do
it "should return unauthorized" do
post '/list'
assert_response :unauthorized
end
end
end
In params you need to pass your list_params.
Spec would look like:
describe "POST create" do
context 'valid request' do
it 'should increase #list item' do
expect { post :create }.to change(List, :count).by(1)
end
it "returns HTTP status" do
post :create
expect(response).to have_http_status :success #200
end
end
context 'invalid request' do
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
get :create
expect(response.status).to eq 400
end
end
end
Cheers!
You can test an object not being created by intentionally causing some of its validations to fail e.g. you can pass a mandatory attribute as nil from the RSpec.
Sample request: post :create, { title: nil }.
But as per your RSpec code, it seems there are no validations on List model. So, lets try to stub save and return false for this particular test.
describe 'GET status if its not created' do
# Assuming your model name is `List`
before { allow_any_instance_of(List).to receive(:save) { false } }
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
post :create
expect(response.status).to eq 400
end
end
Please post your model for list and i can update the answer with more appropriate test.
Ishika, let me see if I can help you :)
RSpec official documentation recommends you to use request specs instead of controller specs. That is recommended because Rails 5 deprecated some methods used on controller testings. You can read more about this here at RSpec blog
ps.: You can use controller tests so far, but it can be deprecated in a future major version of RSpec.
There are some notes I left after the code, please read them also.
I would write a request spec like this:
# spec/requests/v1/lists_controller_create_spec.rb
require "rails_helper"
RSpec.describe V1::ListsController do
describe 'success' do
it 'returns ok and creates a list', :aggregate_failures do # :aggregate_failures is available only for RSpec 3.3+
expect do
post '/list', title: 'foo' # This will also test your route, avoiding routing specs to be necessary
end.to change { List.count }.from(0).to(1)
expect(response).to have_http_status(:ok)
end
end
describe 'bad request' do
before do
# This is needed because your controller is not validating the object, but look at my
# comment below (out of the code), to think about this behavior, please.
allow_any_instance_of(List).to receive(:save).and_return(false)
end
it 'returns a bad request and does not create a list' do
expect do
post '/list', title: 'foo' # This will also test your route, avoiding routing specs to be necessary
end.not_to change { List.count }
expect(response).to have_http_status(:bad_request)
end
end
end
Notes:
I suggested using more than 1 expectation by example, that is ok in this spec because they are simple and because I'm using :aggregate_failures option. With this option, if the first expectation fails, the next expectations will also be executed, considering that in this case, the following expectations does not depend on the first one, it is ok to use more than 1 expectation for the example.Reference
You are returning a bad request if the object is not saved, but you are not validating it. If your model has validations that will validate the object there, please adjust the specs to fail the save (instead of using the mock I used) and consider rendering an error message in the response
If you think that making the post inside a expect block, you can do different: Store the count of Lists in a variable before making the post and after the post you test if the variable has changed or not, maybe you think it will be more clear and it will do exactly the same thing in the background.
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" }
I have a spec type: :request and I want to add authentication via ApiAuth. How can i do that?
ApiAuth use request object for authentication
ApiAuth.sign!(request, user.id, user.api_secret_key)
And I can use it in specs for controllers as follows
request.env['HTTP_ACCEPT'] = 'application/json'
user = defined?(current_user) ? current_user : Factory.create(:admin_user)
ApiAuth.sign!(request, user.id, user.api_secret_key)
But request object is missing in specs for requests
describe 'Cities API', type: :request do
let(:cities) { City.all }
let(:admin_user) { Factory.create(:admin_user) }
context 'given an unauthorized request' do
it 'returns 401 status' do
get '/api/cities'
expect(response).to have_http_status(:unauthorized)
end
end
context 'given an authorized request' do
it 'sends a list of cities' do
# i need authorization here
get '/api/cities'
expect(response).to be_success
end
end
end
Now I stub authentication
describe 'Cities API', type: :request do
let(:cities) { City.all }
let(:admin_user) { Factory.create(:admin_user) }
before { Factory.create(:route) }
context 'given an unauthorized request' do
it 'returns 401 status' do
get '/api/cities'
expect(response).to have_http_status(:unauthorized)
end
end
context 'given an authorized request' do
before(:each) do
allow_any_instance_of(CitiesController).to receive(:authenticate_user!).and_return(true)
allow_any_instance_of(CitiesController).to receive(:admin_user).and_return(admin_user)
end
it 'sends a list of cities' do
get '/api/cities'
expect(response).to be_success
end
end
end
But I want to avoid of using stubs in request specs.
Does anybody have any ideas?
I created issue on github. You can watch my solution here