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.
Related
In a rails 6.1.3.1 API, I would like to implement an unusual test. Basically, a user can create a team with a many-to-many relational model. A team creation automatically triggers a new entry in the table JoinedTeamUser with two references (user_id and team_id), and a boolean can_edit in order to know whether the user can update/delete or not a team details.
What I would like to test is if a user accessing the update or destroy method of the team controller has the right to do so (meaning, the right entry in a model depending of another controller).
To give you a better view of the process, here is the file seed.rb.
3.times do |i|
team = Team.create(name: Faker::Company.name, description: Faker::Company.catch_phrase)
puts "Created TEAM ##{i+1} - #{team.name}"
end
10.times do |i|
name = Faker::Name.first_name.downcase
user = User.create! username: "#{name}", email: "#{name}#email.com", password: "azerty"
puts "Created USER ##{i+1} - #{user.username}"
joined_team_users = JoinedTeamUser.create!(
user_id: user.id,
team_id: Team.all.sample.id,
can_edit: true
)
puts "USER ##{joined_team_users.user_id} joined TEAM ##{joined_team_users.team_id}"
end
Edit:
As requested, here is the team controller (the filter of authorized to update user has still not been implemented) :
class Api::V1::TeamsController < ApplicationController
before_action :set_team, only: %i[show update destroy]
before_action :check_login
def index
render json: TeamSerializer.new(Team.all).serializable_hash.to_json
end
def show
render json: TeamSerializer.new(#team).serializable_hash.to_json
end
def create
team = current_user.teams.build(team_params)
if team.save
if current_user&.is_admin === false
JoinedTeamUser.create!(
user_id: current_user.id,
team_id: team.id,
can_edit: true
)
render json: TeamSerializer.new(team).serializable_hash.to_json, status: :created
else
render json: TeamSerializer.new(team).serializable_hash.to_json, status: :created
end
else
render json: {errors: team.errors }, status: :unprocessable_entity
end
end
def update
if #team.update(team_params)
render json: TeamSerializer.new(#team).serializable_hash.to_json, status: :ok
else
render json: #team.errors, status: :unprocessable_entity
end
end
def destroy
#team.destroy
head 204
end
private
def team_params
params.require(:team).permit(:name, :description, :created_at, :updated_at)
end
def set_team
#team = Team.find(params[:id])
end
end
and the team controller tests :
require "test_helper"
class Api::V1::TeamsControllerTest < ActionDispatch::IntegrationTest
setup do
#team = teams(:one)
#user = users(:one)
end
# INDEX
test "should access team index" do
get api_v1_teams_url,
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert_response :success
end
test "should forbid team index" do
get api_v1_teams_url, as: :json
assert_response :forbidden
end
# SHOW
test "should show team" do
get api_v1_team_url(#team),
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert_response :success
end
# SHOW
test "should forbid show team" do
get api_v1_team_url(#team),
as: :json
assert_response :forbidden
end
# CREATE
test "should create team" do
assert_difference('Team.count') do
post api_v1_teams_url,
params: { team: { name: "Random name", description: "Random description" } },
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
end
assert_response :created
end
test "should not create team when not logged in" do
assert_no_difference('Team.count') do
post api_v1_teams_url,
params: { team: { name: "Random name", description: "Random description" } },
as: :json
end
assert_response :forbidden
end
test "should not create team with taken name" do
assert_no_difference('Team.count') do
post api_v1_teams_url,
params: { team: { name: #team.name, description: "Random description" } },
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
end
assert_response :unprocessable_entity
end
test "should not create team without name" do
assert_no_difference('Team.count') do
post api_v1_teams_url,
params: { team: { description: "Random description"} },
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
end
assert_response :unprocessable_entity
end
# UPDATE
test "should update team" do
patch api_v1_team_url(#team),
params: { team: { name: "New name", description: "New description" } },
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert_response :success
end
test "should not update team " do
patch api_v1_team_url(#team),
params: { team: { name: "New name", description: "New description" } },
as: :json
assert_response :forbidden
end
# DESTROY
test "should destroy team" do
assert_difference('Team.count', -1) do
delete api_v1_team_url(#team),
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
end
assert_response :no_content
end
test "should forbid destroy team" do
assert_no_difference('Team.count') do
delete api_v1_team_url(#team), as: :json
end
assert_response :forbidden
end
end
Thanks !
I would start by actually fixing the models.
class Team < ApplicationRecord
has_many :memberships
has_many :members, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :team
belongs_to :member, class_name: 'User'
end
class User < ApplicationRecord
has_many :memberships,
foreign_key: :member_id
has_many :teams,
through: :memberships
end
Joins should never be a part of the name of your model. Name your models after what they represent in your domain, not as peices of plumbing.
Whatever is_admin is should be admin?.
If you want to create a team and make the user who created the team a member at the same time you want to do:
# See https://github.com/rubocop/ruby-style-guide#namespace-definition
module Api
module V1
class TeamsController < ApplicationController
def create
#team = Team.new(team_params)
#team.memberships.new(
member: current_user,
can_edit: current_user.admin?
)
if team.save
# why is your JSON rendering so clunky?
render json: TeamSerializer.new(team).serializable_hash.to_json, status: :created
else
render json: {errors: team.errors }, status: :unprocessable_entity
end
end
# ...
end
end
end
This will insert the Team and Membership in a single transaction and avoids creating more code paths in the controller.
require "test_helper"
module API
module V1
class TeamsControllerTest < ActionDispatch::IntegrationTest
setup do
#team = teams(:one)
#user = users(:one)
end
# ...
test "when creating a team the creator is added as a member" do
post api_v1_teams_url,
# add valid parameters to create a team
params: { team: { ... } },
# repeating this all over your tests is very smelly
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert #user.team_memberships.exist?(
team_id: Team.last.id
), 'expected user to be a member of team'
end
end
end
end
As for actually adding the authorization checks I would recommend that you look into Pundit instead of reinventing the wheel, this also has a large degree of overlap with Rolify which is a much better alternative then adding a growning of can_x boolean columns.
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
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 writing an RSpec called Leads controller spec. In that I'm writing a test for create action of lead controller. Now my lead controller calls Project model to create an object(Project) which also creates Contact object and assigns it to project. but when I try to test whether my Project model creating a Contact object or no, The tests are getting failed. I don't know why my contact object is not getting created:(
My leads_controller_spec.rb
describe "POST #create" do
it "should create a contact too" do
my_lead = Fabricate(:project, id: Faker::Number.number(10))
expect{
post :create, project: my_lead.attributes
}.to change(Contact, :count).by(1)
end
it "should be equal to last created contact" do
my_lead = Fabricate(:project, id: Faker::Number.number(10))
post :create, project: my_lead.attributes
expect(Project.last.contact).to eq(Contact.last)
end
end
leads_controller.rb
def create
if #lead = Project.add_new_lead(lead_params)
#lead.create_activity :create_new_lead, owner: current_user
puts "My lead in create action: #{#lead.inspect}"
else
respond_to do |format|
format.html { redirect_to :back, :alert => "Email is already Taken"}
end
end
respond_to do |format|
format.html { redirect_to leads_path }
end
end
Project.rb
def add_new_lead(inputs, data = {})
if !Contact.where(email: inputs[:email]).present?
contact = Contact.create(phone: inputs[:phone], email: inputs[:email], fullname: inputs[:fullname])
project = Project.create(name: inputs[:fullname], flat_status: inputs[:flat_status], flat_type: inputs[:flat_type], flat_area: inputs[:area], location: inputs[:locality], address: inputs[:site_address], customer_type: inputs[:customer_type])
project.contact = contact
project.save
project
else
return nil
end
end
contact_fabricator.rb
require 'faker'
Fabricator(:contact) do
email { "email_#{Kernel.rand(1..30000)}#prestotest.com" }
fullname "project#{Kernel.rand(1..30000)}"
address "address#{Kernel.rand(1..30000)}"
end
project_fabricator.rb
require 'faker'
Fabricator(:project) do
contact
end
contact.rb
field :phone, type: String
field :email, type: String
field :fullname, type: String
field :status, type: String, default: "DEFAULT"
field :address, type: String
field :new_address, type: String
field :other_data, type: Hash, default: {}
validates_presence_of :email
validates_uniqueness_of :email, :message => "Email already taken"
You are using the factories wrong in your spec. You want to use Fabricate.attributes_for(:project) instead of creating a record and taking its attributes as that will cause any uniqueness validations to fail.
require 'rails_helper'
describe ProjectsContoller
describe "POST #create" do
# don't forget to test with invalid input!
context "with invalid attributes" do
let(:attributes) { { foo: 'bar' } }
it "does not create a project" do
expect do
post :create, attributes
end.to_not change(Project, :count)
end
end
context "with valid attributes" do
let(:attributes) { Fabricate.attributes_for(:project) }
it "creates a project" do
expect do
post :create, attributes
end.to change(Project, :count).by(+1)
end
end
end
end
When it comes to the rest of your controller you should very likely be using nested attributes instead as you are not handling the case where the validation of contact fails. You're also using Contact.create when you should be using Contact.new.
Note that this is a pretty advanced topic and you might want to learn the basics first and revisit it later.
class Project < ActiveRecord::Base
belongs_to :contact
accepts_nested_attributes_for :contact
validates_associated :contact
end
class Contact < ActiveRecord::Base
has_many :projects
end
class ProjectsController < ApplicationController
def new
#project = Project.new
end
def create
#project = Project.new(project_params)
if #project.save
format.html { render :new } # don't redirect!
else
format.html { redirect_to leads_path }
end
end
private
def project_params
params.require(:project).permit(:foo, :bar, contact_attributes: [:email, :name, :stuff, :more_stuff])
end
end
<%= form_for(#project) do |f| %>
<%= f.text_field :foo %>
# These are the attributes for the contact
<%= fields_for :contact do |pf| %>
<%= pf.text_field :email %>
<% end %>
<% end %>