Cross controllers test in Rails - ruby-on-rails

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.

Related

How can i validate some attributes?

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.

Testing comments controller. I cant pass test for create comment

I want to test comments controller, action create, but I don't know how what's wrong with it. Test are not save comment
comments_controller.rb is work in my projetc, i can see all comments by rails console (as like Comments.all. So that valid:
class CommentsController < ApplicationController
before_action :authenticate_user!, only:[:create,:vote]
before_action :show, only:[:show,:vote]
respond_to :js, :json, :html
def create
#comment = Comment.new(comment_params)
#comment.user_id = current_user.id
if #comment.save
redirect_to post_path(#comment.post.id)
else
redirect_to root_path
end
end
private
def comment_params
params.require(:comment).permit(:comment, :post_id)
end
end
comments_controller_spec.rb is here. It seems like that i send bad params
require 'rails_helper'
RSpec.describe CommentsController, type: :controller do
let(:user) {create :user}
let(:params) { {user_id: user} }
before {sign_in user}
describe '#create' do
let(:post) {create :post}
let(:params) do
{
user_id: user.id,
post_id: post.id,
comment: attributes_for(:comment)
}
end
subject {process :create, method: :post, params: params}
it 'create comment' do
expect{subject}.to change {Comment.count}.by(1)
# is_expected.to redirect_to(user_post_path(assigns(:user), assigns(:post)))
end
end
end
factory comments.rb is here:
FactoryBot.define do
factory :comment do
association :post
association :user
user_id { FFaker::Internet.email }
comment { FFaker::Lorem.sentence }
end
end

Factory Bot Failures in Rails App

So I have the following models: Regions and Locations. They look fine and I can load up the page just fine. The issue gets with factory bot.
class Region < ApplicationRecord
scope :ordered, -> { order(name: :desc) }
scope :search, ->(term) { where('name LIKE ?', "%#{term}%") }
has_many :locations, dependent: :destroy
end
class Location < ApplicationRecord
scope :ordered, -> { order(name: :desc) }
scope :search, ->(term) { where('name LIKE ?', "%#{term}%") }
belongs_to :region
end
For my factories I have the following:
FactoryBot.define do
factory :region do
name 'MyString'
end
end
FactoryBot.define do
factory :location do
name 'MyString'
hours_operation 'MyString'
abbreviation 'MyString'
region nil
end
end
Both of those were rendered easily enough and the only change I made to them was switching out double quotations with single quotations. But when I run a bin/test I keep getting the following errors: Admin::LocationsController show should render the show page Failure/Error: location = FactoryBot.create(:location) ActiveRecord::RecordInvalid:Validation failed: Region must exist
Then a coordinating example of the failure: rspec ./spec/controllers/admin/locations_controller_spec.rb:16 # Admin::LocationsController show should render the show page
So I check out spec/controllers/admin/locations_controller_spec.rb and here's the code:
require 'rails_helper'
RSpec.describe Admin::LocationsController, type: :controller do
before(:each) do
activate_session(admin: true)
end
describe 'index' do
it 'should render the index' do
get :index
expect(response).to have_http_status :success
end
end
describe 'show' do
it 'should render the show page' do
location = FactoryBot.create(:location)
get :show, params: { id: location.id }
expect(response).to have_http_status :success
end
it 'should return a 404 error' do
get :show, params: { id: 1 }
expect(response).to have_http_status :not_found
end
end
describe 'new' do
it 'should render the new page' do
get :new
expect(response).to have_http_status :success
end
end
describe 'edit' do
it 'should render the edit page' do
location = FactoryBot.create(:location)
get :edit, params: { id: location.id }
expect(response).to have_http_status :success
end
end
describe 'create' do
it 'should create the record' do
expect do
post :create, params: { location:
FactoryBot.attributes_for(:location) }
end.to change(Location, :count).by(1)
end
end
describe 'update' do
it 'should update the record' do
location = FactoryBot.create(:location)
current_value = location.name
new_value = current_value + '-Updated'
expect do
patch :update, params: { id: location.id, location: { name: new_value } }
location.reload
end.to change(location, :name).to(new_value)
end
end
describe 'destroy' do
it 'should destroy the record' do
location = FactoryBot.create(:location)
expect do
delete :destroy, params: { id: location.id }
end.to change(Location, :count).by(-1)
end
end
end
The line it's references is: it 'should render the show page'.
Honestly I don't know what's causing it to fail. I thought it might be an association issues but when I went to try and do something like associations :location it caused even more errors to populate. Am I just missing something in the spec controllers for association?
EDIT/UPDATE: Fixed five of the six errors using association :region in location. But I'm getting: Failure/Error: expect do post :create, params: { location: FactoryBot.attributes_for(:location) } end.to change(Location, :count).by(1) expected #count to have changed by 1, but was changed by 0.
You can create associations in factory-bot using after_create, try this
FactoryBot.define do
factory :location do
name 'MyString'
hours_operation 'MyString'
abbreviation 'MyString'
after(:create) do |location, evaluator|
location.region = FactoryBot.create(:region)
end
end
end
All this is doing is creating a region object and associating it to the location object.
See here for more detailed guides Factory Bot Guides

Testing that a user can't update/destroy another user's comment

In my small app , users can post comments. These comments can be destroyed only by their owners. I am trying to log in a user, create a comment, log out a user and then try to delete the comment that the first user created. However this action succeds for some reason. This is my comments controllor only showing the create and update actions and private methods:
module Api
module V1
class CommentsController < Api::V1::BaseController
before_action :check_user
before_action :get_comment, only: [:destroy, :update]
respond_to :json
def destroy
if #comment.destroy
head :no_content, status: :no_content
else
render json: serialize_model(#comment.errors)
end
end
def update
if #comment.update_attributes(comment_params)
render json: serialize_model(#comment), status: :accepted
else
render json: { errors: #comment.errors }, status: :bad_request
end
end
private
def comment_params
params.require(:comment).permit(:text, :picture_id)
end
def get_comment
#comment = Comment.find_by_id(params[:id])
check_owner
end
def check_user
render json: { errors: { user: "not signed in" } }, status: :unauthorized unless user_signed_in?
end
def check_owner
render json: { errors: { user: "not the owner" } }, status: :unauthorized unless current_user.id = #comment.id
end
end
end
end
These are my shared exmples for the test:
shared_context 'comments' do
def setup_requirements_without_login
#user = FactoryGirl.create(:user)
#category = FactoryGirl.create(:category)
#picture = FactoryGirl.create(:picture, category_id: #category.id, user_id: #user.id)
end
def setup_requirements_with_login
setup_requirements_without_login
sign_in(#user)
end
shared_examples 'not the owner' do
it 'creates a resource' do
body = JSON.parse(response.body)
expect(body).to include('errors')
data = body['errors']
expect(data).to include('user')
end
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
end
And these are the tests for update and destroy action:
require "rails_helper"
include Warden::Test::Helpers
Warden.test_mode!
RSpec.describe Api::V1::CommentsController, type: :controller do
include_context 'comments'
describe 'PATCH /api/comments/:id' do
context 'when it is a valid request' do
let(:attr) do
{ text: 'update' }
end
before(:each) do
setup_requirements_with_login
#comment = FactoryGirl.create(:comment, picture_id: #picture.id, user_id: #user.id)
patch :update, id: #comment.id, comment: attr , format: :json
end
it 'creates a resource' do
body = JSON.parse(response.body)
expect(body).to include('data')
data = body['data']
expect(data['attributes']['text']).to eq('update')
end
it 'responds with 202' do
expect(response).to have_http_status(202)
end
end
context 'when the user is not logged in' do
let(:attr) do
{ text: 'update' }
end
before(:each) do
setup_requirements_without_login
#comment = FactoryGirl.create(:comment, picture_id: #picture.id, user_id: #user.id)
patch :update, id: #comment.id, comment: attr , format: :json
end
it_behaves_like "not logged in"
end
context 'when the user is not the owner' do
let(:attr) do
{ text: 'update' }
end
before(:each) do
setup_requirements_with_login
#comment = FactoryGirl.create(:comment, picture_id: #picture.id, user_id: #user.id)
sign_out(#user)
logout
#user2 = FactoryGirl.create(:user)
sign_in(#user2)
patch :update, id: #comment.id, comment: attr , format: :json
end
it_behaves_like "not the owner"
end
end
describe 'DELETE /api/comments/:id' do
context 'when it is a valid request' do
before(:each) do
setup_requirements_with_login
#comment = FactoryGirl.create(:comment, picture_id: #picture.id, user_id: #user.id)
delete :destroy, id: #comment.id, format: :json
end
it 'responds with 204' do
expect(response).to have_http_status(204)
end
end
context 'when the user is not logged in' do
before(:each) do
setup_requirements_without_login
#comment = FactoryGirl.create(:comment, picture_id: #picture.id, user_id: #user.id)
delete :destroy, id: #comment.id, format: :json
end
it_behaves_like "not logged in"
end
context 'when the user is not the owner' do
before(:each) do
setup_requirements_with_login
#comment = FactoryGirl.create(:comment, picture_id: #picture.id, user_id: #user.id)
sign_out(#user)
logout
#user2 = FactoryGirl.create(:user)
sign_in(#user2)
delete :destroy, id: #comment.id, format: :json
end
it_behaves_like "not the owner"
end
end
end
My problem is that the action succeeds when it shouldn't for some reason. I use pry to debugg and it makes me question the tests even more because it says current_user has the id of 97 when the test created users with the ids: 1001 and 1002 which is very odd... . Did I make a mistake in the controller ? or tests?
your check_owner function should have == instead of = in its unless condition:
unless current_user.id == #comment.id
Otherwise the id from the #comment gets assigned to current_user.id. This is probably the origin for your 97. =)
def get_comment
#comment = current_user.comments.find! params[:id]
end
This automatically adds the association to the SQL query (where user_id=1337) and the bang method (with the !) throws an 404 Exception if record wasnt found. That is the easiest way to controll that only the owner has access to its own records.

RSpec + FactoryGirl and controller specs

I'm trying to add FactoryGirl support the default scaffolded specs in my controllers and I can't seem to find the correct syntax.
Here's an example test:
describe "POST create" do
describe "with valid params" do
it "creates a new Course" do
expect {
post :create, {:course => valid_attributes}, valid_session
}.to change(Course, :count).by(1)
end
do
Which I replace by:
describe "POST create" do
describe "with valid params" do
it "creates a new Course" do
expect {
post :create, course: FactoryGirl.build(:course)
}.to change(Course, :count).by(1)
end
do
spec/factories/courses.rb
FactoryGirl.define do
factory :course do
association :provider
name "Course name"
description "course description"
end
end
app/models/course.rb
class Course < ActiveRecord::Base
validates :name, :presence => true
validates :description, :presence => true
validates :provider_id, :presence => true
has_many :terms
belongs_to :provider
end
app/controllers/courses_controller.rb
# POST /courses
# POST /courses.json
def create
#course = Course.new(course_params)
respond_to do |format|
if #course.save
format.html { redirect_to #course, notice: 'Course was successfully created.' }
format.json { render action: 'show', status: :created, location: #course }
else
format.html { render action: 'new' }
format.json { render json: #course.errors, status: :unprocessable_entity }
end
end
end
It usually fails with: "ActionController::ParameterMissing: param not found: course" does anyone have any idea?
Thanks!
Francis
Try:
describe "POST create" do
describe "with valid params" do
it "creates a new Course" do
expect {
post :create, course: FactoryGirl.attributes_for(:course, provider_id: 1)
}.to change(Course, :count).by(1)
end
end
end
Factory Girl uses attributes_for option to generate a hash of values as opposed to a Ruby object.

Resources