I am using Rails to write a controller receiving JSON as its params, and then testing it with an Rspec request spec.
controller:
class API::UserController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
redirect_to #user
else
render json: { errors: #user.errors.full_messages }, status: :unprocessable_entity
end
private
def user_params
params.require(:user).permit(:username, :email, :name, :password_digest)
end
end
spec:
RSpec.describe "User creation", type: :request do
def create_user_request
params = { user: build(:user), format: :json }
post api_users_path, params: params.to_json
end
it 'should create a new user' do
expect { create_user_request }.to change { User.count }.by(1)
end
But this fails with:
Failure/Error: params.require(:user).permit(:username, :name, :email, :password_digest)
ActionController::ParameterMissing:
param is missing or the value is empty: user
I think the issue is that params in the controller is a serialized JSON object as a string. If I replace params: params.to_json with params: params in my spec, then the user params becomes the string "#<User:0x.....>".
How do I make the spec and the controller play nice together?
I think the first issue is that you need to convert the model into a hash (not stringify it). The easiest way to do that is using .attributes i.e.
build(:user).attributes
I'm also not sure if you should be passing format: :json as part of the params, but I think it will depend on which version of rails, rspec etc you are using, but for rails 4 what works for me is
post api_users_path, user: build(:user).attributes, format: :json
Related
How can i validate if params have 'name' and 'section'? for example: i want to validate 'name' but if there is not then i have to return 400, same with 'section'
context 'validation' do
let!(:params) do
{ article: {
name: 'a1',
section: 'A'
...
color: 'red'
} }
end
i dont know how can i compare
it 'test, not allow empty name' do
expect(name eq '').to have_http_status(400)
end
While you could check the parameters directly:
def create
if params[:article][:name].blank? || params[:article][:section].blank?
return head 400
end
# ...
end
The Rails way of performing validation is through models:
class Article < ApplicationRecord
validates :name, :section, presence: true
end
class ArticlesController < ApplicationController
# POST /articles
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article, status: :created
else
# Yes 422 - not 400
render :new, status: :unprocessable_entity
end
end
private
def article_params
params.require(:article)
.permit(:name, :section, :color)
end
end
require 'rails_helper'
RSpec.describe "Articles API", type: :request do
describe "POST /articles" do
context "with invalid parameters" do
it "returns 422 - Unprocessable entity" do
post '/articles',
params: { article: { name: '' }}
expect(response).to have_http_status :unproccessable_entity
end
end
end
end
This encapsulates the data together with validations that act on the data and validation errors so that you display it back to the user.
Models (or form objects) can even be used when the data isn't saved in the database.
I am trying to stub out an :authenticate_user method call in my request spec so I can test the user's association creation. I am using these blog posts as a guide on stubbing:
1) https://8thlight.com/blog/mike-knepper/2014/07/01/stubbing-authentication-and-authorization-in-controller-specs.html
2) http://johnnyji.me/rspec/2015/06/18/stubbing-controller-instance-methods-in-rspec.html
I'm not having any success with stubbing and I can't figure out what am I missing.
When I tried
it 'creates a new contract' do
allow(controller).to receive(:authenticate_user).and_return(user)
post api_v1_user_contracts_path(user), { params: contract_params}
expect(response).to have_http_status(200)
end
I got:
When I tried:
it 'creates a new contract' do
allow_any_instance_of(controller).to receive(:authenticate_user).and_return(user)
post api_v1_user_contracts_path(user), { params: contract_params}
expect(response).to have_http_status(200)
end
I got
My code:
spec/requests/contracts_api_spec.rb
require 'rails_helper'
require 'pry'
context "POST #create" do
let (:user) { User.create(full_name: "Jason Bourne", email: "jbourne#test.com", password: "123456") }
let (:contract_params) do
{
"contract[vendor]" => "Lebara",
"contract[starts_on]" => "2018-12-12",
"contract[ends_on]" => "2018-12-16",
"contract[price]" => "15"
}
end
it 'creates a new contract' do
allow(controller).to receive(:authenticate_user).and_return(user)
post api_v1_user_contracts_path(user), { params: contract_params}
expect(response).to have_http_status(200)
end
app/controllers/api/v1/contracts_controller.rb
class Api::V1::ContractsController < ApplicationController
before_action :authenticate_user
def show
if #current_user.contracts.find_by(id: params[:id])
render json: #current_user.contracts.find_by(id: params[:id])
else
render json: { error: "Contract not found"}, status: 400
end
end
def create
contract = #current_user.contracts.build(contract_params)
if contract.save
render json: contract
else
render json: { error: contract.errors }, status: 400
end
end
app/controllers/concerns/token_authenticatable.rb
class NotAuthorizedException < StandardError; end
module TokenAuthenticatable
extend ActiveSupport::Concern
included do
attr_reader :current_user
before_action :authenticate_user
rescue_from NotAuthorizedException, with: -> { render json: { error: 'Not Authorized' }, status: :unauthorized }
end
private
def authenticate_user
#current_user = DecodeAuthenticationCommand.call(request.headers).result
raise NotAuthorizedException unless #current_user
end
end
Additional questions:
1) Should I be using a real User object, or should that be a double? I'm assuming it should be a real user in order to test if the association creation is working.
2) Should I be using allow(Api::V1::ContractsController).to receive(:authenticate_user).and_return(user)? I've tried it before and didn't work but I didn't know it was because something else also was breaking it.
Thanks for any feedback you can give!
The point is that authenticate_user assigns user to the variable (and you use it later). Please try:
allow(DecodeAuthenticationCommand).to receive_message_chain(:call, :result).and_return(user)
With the test double, you will have to define all methods for the user, such as contracts. Also, you are checking if the contract was created - in my opinion, it is perfectly fine to use a real object for the user.
so i've been trying to make a basic response test - with 200 and 403. I'm not sure i need to add anything else ..
accounts_spec.rb
RSpec.describe Api::V1::AccountsController, :type => :controller do
describe "GET index no account" do
it "has a 403 status code" do
get :index
expect(response.status).to eq(403)
end
end
describe "GET index with account" do
login_user
it "has a 200 status code" do
get :index
expect(response.status).to eq(200)
end
end
end
Accounts Controller #index
def index
#show user details
raise if not current_user
render json: { :user => current_user.as_json(:except=>[:created_at, :updated_at, :authorization_token, :provider, :uid, :id])}
rescue
render nothing: true, status: 403
end
I keep getting
1)Api::V1::AccountsController GET index with account has a 200 status code
expected: 200
got: 403
Any thoughts on where i'm doing it wrong ?
UPDATE
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user)
sign_in :user, user
end
end
end
Much cleaner implementation
class SomeController < ApplicationController
before_action :authenticate
skip_before_action :verify_authenticity_token, if: :json_request?
def index
render json: { user: current_user...
end
protected
def json_request?
request.format.json?
end
def authenticate
head :unauthorized unless current_user
end
end
I also recommend in using ActiveModel Serializer https://github.com/rails-api/active_model_serializers. This will separate logic of render json and oy will have a separate class under serializer that defines the json output. So your render method will look like this:
render json: current_user, status: :ok
app/serializers/user.rb
class UserSerializer < ActiveModel::Serializer
attribute :id, :email #list your attributes for json output
end
If you want to test json response in your rspec what I find best is testing against json schema like this library https://github.com/sharethrough/json-schema-rspec.
Hope it helps
I'm having a perplexing problem where my controller is working fine. However, when I'm testing it with RSPEC it's returning an empty string as the response body.
Here is the controller:
class Api::UsersController < Api::ApplicationController
def show
#user = User.find(params[:id])
render 'show', status: 200
# render json: #user
end
end
And the RABL template I'm rendering:
object #user
attributes :id, :name, :email, :phone_number, :invite_token
Finally here is my spec:
require "spec_helper"
describe Api::UsersController do
it "returns user attributes" do
user = FactoryGirl.create(:user, name: "Mark", email: "foo#bar.com")
get :show, id: user.id
expect(response.status).to eq(200)
output = JSON.parse(response.body)
end
end
When I use render 'show' to render the RABL template my test fails as the response.body is an empty string. However, if I CURL to that endpoint, the body returns just fine.
When I change the controller to: render json: #user the test passes.
Can anyone tell me what's going on here?
Thanks in advance!
try to add render_views at the top of the tests
describe Api::UsersController do
render_views
it "returns user attributes" do
user = FactoryGirl.create(:user, name: "Mark", email: "foo#bar.com")
get :show, id: user.id
output = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(output).to eq(expected_hash)
end
end
Possible reason: RSpec do not render views by default to speed up tests.
I have the following in my Registrations Controller:
class Api::V1::RegistrationsController < ApplicationController
skip_before_filter :verify_authenticity_token
respond_to :json
def create
user = User.new(params[user_params])
if user.save
render :json => user.as_json(:auth_token=>user.authentication_token, :email=>user.email), :status=>201
return
else
warden.custom_failure!
render :json => user.errors, :status=>422
end
end
def user_params
params.require(:user).permit(:email, :password, :name, :phone, :acknowledgement)
end
end
I realize this expects my JSON to be in the form of
{user:{email:user#example.com,name:"anotheruser"}}
However the JSON is being sent as
{email:user#example.com, name:"anotheruser"}
I don't know how to target those params. What is the syntax for that?
Also, is there a special way to handle that format?
The expected json you got would be a hash of hashes, but you are only getting back a hash, is what it sounds like.
Given this, you don't need to require :user since :user isn't there. Just do this.
params.permit(:email, :password, :name, :phone, :acknowledgement) if params.present?
present? will check to ensure params is not nil or blank/empty. You could raise an error, if you wanted, if the present? check fails.