Reputation: 166
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 !
Upvotes: 0
Views: 59
Reputation: 102036
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.
Upvotes: 1