I must be doing something wrong or I need glasses. I am following this tutorial here:
http://vaidehijoshi.github.io/blog/2015/09/29/using-pundit-the-cool-kid-of-authorization/
I've created the application_policy.rb file and user_policy.rb files inside the app/policies folder as instructed.
Pundit doesn't seem to detect my UserPolicy file inside IntegrationTests.
My Setup
Ruby on Rails 5 RC1
Ruby 2.2.4p230
Knock gem for JWT authentication
Pundit 1.1.0
user_policy.rb
class UserPolicy < ApplicationPolicy
def update?
user == resource
end
end
In my UserController, I have the defined REST API actions. Currently, I'm just testing the "update" action:
UsersController
def update
user = User.find_by({id: params[:id]})
authorize user
render json: { error: "Failed to find" }, status: :not_found and return unless user
if user.update!(user_params)
render json: user
else
render json: { error: "Not found" }, status: :not_found
end
end
users_controller_test.rb
test "update other user's data should raise NotAuthorized exception" do
#user_two = users(:two)
put user_path(#user_two.id), params: { first_name: "Jim" }, headers: #authorization_header
assert_response :success # forcing fail test for now to test plumbing
end
I am getting the following errors:
.E
Error:
UsersControllerTest#test_update_other_user's_data_should_raise_NotAuthorized_exception:
Pundit::NotDefinedError: unable to find policy `UserPolicy` for `#<User id: 298486374, first_name: "MyString", last_name: "MyString", email: "MyString", password_digest: "MyString", created_at: "2016-05-27 13:30:07", updated_at: "2016-05-27 13:30:07", role_id: nil>`
app/controllers/users_controller.rb:42:in `update'
test/controllers/users_controller_test.rb:60:in `block in <class:UsersControllerTest>'
Any ideas what I might be doing wrong?
Edit
If it's any help, my UserControllerTest file looks like this:
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def authenticate
token = Knock::AuthToken.new(payload: { sub: users(:one).id }).token
#authorization_header = { HTTP_AUTHORIZATION: "Bearer #{token}" }
end
setup do
# load "#{Rails.root}/db/seeds.rb"
Rails.application.load_seed
authenticate
#user = users(:one)
end
test "logged in user should return ok" do
get users_path, headers: #authorization_header
assert_response :ok
end
test "not logged in user should return unauthorized" do
get users_path
assert_response :unauthorized
end
test "user json should not contain password_digest" do
get user_path(#user.id), headers: #authorization_header
assert_response :ok
json = JSON.parse(response.body)
assert json.key?("password_digest") == false
end
test "create user without authorization header should return created" do
user = {
first_name: "Bob",
last_name: "Brown",
email: "bob#gmail.com",
password: "abc",
password_confirmation: "abc"
}
post users_path, params: user
assert_response :created
json = JSON.parse(response.body)
assert !json.empty?
end
test "update user should return ok" do
put user_path(#user.id), params: { first_name: "Bob"}, headers: #authorization_header
assert_response :ok
updated_user = User.find_by({id: #user.id})
assert_equal "Bob", updated_user.first_name
end
test "update other user's data should raise NotAuthorized exception" do
#user_two = users(:two)
put user_path(#user_two.id), params: { first_name: "Jim" }, headers: #authorization_header
assert_response :success
end
test "delete user shoudl return ok" do
assert_difference "User.count", -1 do
delete user_path(#user.id), headers: #authorization_header
end
end
end
All my tests were passing before I added the Pundit stuff an hour ago.
To fix, I ran:
bin/spring stop
bin/spring binstub --remove --all
bundle update spring
bundle exec spring binstub --all
Okay....I don't know what the heck happened but apparently, I quit all my Mac terminal AND Atom IDE where I typed my code.
Then I opened my terminal and did a rails test command again, and this time it worked:
....E
Error:
UsersControllerTest#test_update_other_user's_data_should_raise_NotAuthorized_exception:
Pundit::NotAuthorizedError: not allowed to update? this #<User id: 298486374, first_name: "MyString", last_name: "MyString", email: "MyString", password_digest: "MyString", created_at: "2016-05-27 15:19:51", updated_at: "2016-05-27 15:19:51", role_id: nil>
app/controllers/users_controller.rb:42:in `update'
test/controllers/users_controller_test.rb:60:in `block in <class:UsersControllerTest>'
Bizarre...
TL;DR
Quit your IDE and Terminal
Reopen your Terminal, then try again
Related
I was working on my rails application's tests and noticed some of my tests were failing after I added a login feature, since the views use the current user_id from the session variable, which was undefined during testing.
I tried to remedy this by creating a post request to create a user (a user can be a professor or a student for my app) and then to login with that user inside the test:
courses_controller_test.rb
setup do
#course = courses(:one)
#user = professors(:admin)
end
test "should get new" do
professor_http_code = post professors_path, params: {professor: {firstname:#user.firstname,
lastname: #user.lastname,
email: #user.email,
password: "123456",
password_confirmation: "123456"}}
puts "Professor post http code: " + professor_http_code.to_s
login_http_code = post login_path, params: {email: #user.email,
password: "123456",
type: {field: "professor"}}
puts "Login post http code: " + login_http_code.to_s
get new_course_url
assert_response :success
end
The test fails with the same problem (no current user when rendering the view) and produces the following output in the console:
Console output
Running via Spring preloader in process 22449
Run options: --backtrace --seed 26071
# Running:
.Professor create params: <ActionController::Parameters {"firstname"=>"foo", "lastname"=>"bar", "email"=>"foobar#gmail.com", "password"=>"123456", "password_confirmation"=>"123456"} permitted: false>
Professor not saved to db
..Professor post http code: 200
user login params: #<Professor id: 135138680, firstname: "foo", lastname: "bar", email: "foobar#gmail.com", created_at: "2020-03-31 02:12:50", updated_at: "2020-03-31 02:12:50", password_digest: nil>
Login post http code: 500
F
Failure:
CoursesControllerTest#test_should_get_new [/home/sruditsky/Homework/Capstone/team-formation-app/test/controllers/courses_controller_test.rb:25]:
Expected response to be a <2XX: success>, but was a <500: Internal Server Error>
And here are my session and professor controller functions which are handling the requests:
Professors Controller
class ProfessorsController < ApplicationController
...
def create
#professor = Professor.new(professor_params)
puts "Professor create params: " + params[:professor].inspect
respond_to do |format|
if #professor.save
puts "password_d: " + #professor.password_digest
log_in(#professor, "professor")
format.html { redirect_to #professor, notice: 'Professor was successfully created.' }
format.json { render :show, status: :created, location: #professor }
else
puts "Professor not saved to db"
format.html { render :new }
format.json { render json: #professor.errors, status: :unprocessable_entity }
end
end
end
...
# Never trust parameters from the scary internet, only allow the white list through.
def professor_params
params.require(:professor).permit(:firstname, :lastname, :email, :password, :password_confirmation)
end
Sessions Controller
class SessionsController < ApplicationController
...
def create
user = nil
type = params[:type][:field]
if type == "student"
user = Student.find_by_email(params[:email])
elsif type == "professor"
user = Professor.find_by_email(params[:email])
end
puts "user login params: " + user.inspect
if user && user.authenticate(params[:password])
puts "logging in"
log_in(user, type)
redirect_to root_url, notice: "Logged in!"
else
puts "invalid password"
flash.now[:alert] = "Email or password is invalid"
render "new"
end
end
...
The console output shows that the professor is not being saved to the database, but creating a professor account on the application works fine, and also when I type the following into the rails console in the test env it works fine:
app.post "/professors", params: {professor: {firstname: "foo", lastname: "bar", email: "foobar#gmail.com", password: "123456", password_confirmation: "123456"}}
I have tried adding a random authenticity_token to the params, hardcoding all the strings in the params instead of using the #user object, and dropping and recreating, migrating, loading, and preparing my test database and have had no luck.
Github repo: https://github.com/ditsky/team-formation-app
Let me know if you need to see something else in my application to solve the problem, and any help would be super appreciated!
I was able to fix this by adding a password digest field to my Professor fixture and skipping the creation of a user all together and instead just logging in. I am still not sure why creating the professor by posting to the professors_path did not work in my case so if anyone knows the answer to that please let me know.
I set up Pundit to guard a bunch of request paths, and it's working fine. In particular, if I hit /api/users/:id with a PATCH request, passing the relevant parameters, I get a 403 if I'm not authenticated. Then I wrote this spec
context 'When logged out' do
describe 'user update' do
before(:each) do
#new_user = FactoryBot.create(:user, password: 'testpassword')
end
it 'fails with PATCH' do
patch "/api/users/#{#new_user.id}", params: { given_name: 'Testing Alice' }
expect(response).to have_http_status(:forbidden)
end
end
end
but when I run rspec, I get the following failure:
1) When logged out user update fails with PATCH
Failure/Error: authorize #user
Pundit::NotAuthorizedError:
not allowed to update? this #<User id: 24, email: "nolanschinner#schuster.net", given_name: "Deja", family_name: "Schmeler", role: "USER", password_digest: "$2a$04$3lhKjBj2DfLymYnTfhDZV.IrlhPPxsPHIe.hI0lHdb1...", created_at: "2018-12-07 15:08:00", updated_at: "2018-12-07 15:08:00", verification_token: nil, email_verified: false, gender: nil>
# /Users/morpheu5/.rvm/gems/ruby-2.5.1/gems/pundit-2.0.0/lib/pundit.rb:209:in `authorize'
# ./app/controllers/api/v1/users_controller.rb:55:in `update'
# ...
The method being tested is right here.
As far as I can tell, Pundit raises the exception and this throws rspec into despair. How do I write this test so that it actually works?
The subject is a bit old but, for those still searching for the answer, you should write something like:
context 'When logged out' do
describe 'user update' do
before(:each) do
#new_user = FactoryBot.create(:user, password: 'testpassword')
end
it 'fails with PATCH' do
expect{patch "/api/users/#{#new_user.id}", params: { given_name: 'Testing Alice' }}.to raise_error(Pundit::NotAuthorizedError)
end
end
end
As the title suggests I'm just trying to test the create action in my API controller with RSpec. The controller looks something like:
module Api
module V1
class BathroomController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:create]`
def create
bathroom = Bathroom.new(bathroom_params)
bathroom.user = current_user
if bathroom.save
render json: { status: 'SUCCESS', message: 'Saved new bathroom', bathrooms: bathroom }, status: :ok
end
end
private
def bathroom_params
params.require(:bathroom).permit(:establishment, :address, :city, :state, :zip, :gender, :key_needed, :toilet_quantity)
end
end
end
end
Right now this is doing exactly what it should which is great. The test however...not so much. Here's what I have for the test portion:
describe "POST #create" do
let!(:bath) {{
establishment: "Fake Place",
address: "123 Main St",
city: "Cityton",
state: "NY",
zip: "11111",
gender: "Unisex",
key_needed: false,
toilet_quantity: 1
}}
let!(:params) { {bathroom: bath} }
it "receives bathroom data and creates a new bathroom" do
post :create, params: params
bathroom = Bathroom.last
expect(bathroom.establishment).to eq "Fake Place"
end
end
I'm sure there's more than one thing wrong here but I'm having trouble finding much information about the right way to go about testing this. Any insight or suggestions would be greatly appreciated.
I would skip controller specs altogether. Rails 5 has pretty much delegated ActionController::TestCase (which RSpec wraps as controller specs) to the junk drawer. Controller tests don't send real http requests and stub out key parts of Rails like the router and middleware. Total depreciation and delegation to a separate gem will happen pretty soon.
Instead you want to use a request spec.
RSpec.describe "API V1 Bathrooms", type: 'request' do
describe "POST /api/v1/bathrooms" do
context "with valid parameters" do
let(:valid_params) do
{
bathroom: {
establishment: "Fake Place",
address: "123 Main St",
city: "Cityton",
state: "NY",
zip: "11111",
gender: "Unisex",
key_needed: false,
toilet_quantity: 1
}
}
end
it "creates a new bathroom" do
expect { post "/api/v1/bathrooms", params: valid_params }.to change(Bathroom, :count).by(+1)
expect(response).to have_http_status :created
expect(response.headers['Location']).to eq api_v1_bathroom_url(Bathroom.last)
end
it "creates a bathroom with the correct attributes" do
post "/api/v1/bathrooms", params: valid_params
expect(Bathroom.last).to have_attributes valid_params[:bathroom]
end
end
context "with invalid parameters" do
# testing for validation failures is just as important!
# ...
end
end
end
Also sending a bunch of junk like render json: { status: 'SUCCESS', message: 'Saved new bathroom', bathrooms: bathroom }, status: :ok is an anti-pattern.
In response you should just send a 201 CREATED response with a location header which contains a url to the newly created resource or a response body that contains the newly created resource.
def create
bathroom = current_user.bathrooms.new(bathroom_params)
if bathroom.save
head :created, location: api_v1_bathroom_url(bathroom)
else
head :unprocessable_entity
end
end
If your client can't tell by looking at the response code if the response is successful or not you're doing it wrong.
You don't really need to test the values from the record saved on the database, you could do something like:
expect(post :create, params: params).to change(Bathroom, :count).by(1)
That's enough to test that the create action creates a record on the desired table.
Then you can add more specs to test that Bathroom.new receives the expected parameters (that way you know that it would have those fields when saved), or stub the bathroom object and it's save method to test the response.
If you want to test that the saved record has the right values, I think that spec belongs to the Bathroom model and not the controller (or better, an integration test).
So I followed the advice of max but made one slight change to get it working. My final code was:
RSpec.describe "API V1 Bathrooms", type: 'request' do
describe "POST /api/v1/bathrooms" do
context "with valid parameters" do
let(:valid_params) do
{
bathroom: {
establishment: "Fake Place",
address: "123 Main St",
city: "Cityton",
state: "NY",
zip: "11111",
gender: "Unisex",
key_needed: false,
toilet_quantity: 1
}
}
end
it "creates a new bathroom" do
user = FactoryGirl.create(:user, email: "email1#website.com")
login_as(user, :scope => :user)
expect { post "/api/v1/bathrooms", params: valid_params }.to change(Bathroom, :count).by(+1)
expect(response).to have_http_status :created
expect(response.headers['Location']).to eq api_v1_bathroom_url(Bathroom.last)
end
it "creates a bathroom with the correct attributes" do
user = FactoryGirl.create(:user, email: "email2#website.com")
login_as(user, :scope => :user)
post "/api/v1/bathrooms", params: valid_params
expect(Bathroom.last).to have_attributes valid_params[:bathroom]
end
end
end
end
The key was to use FactoryGirl to create a new user because the bathroom needs an associated user_id to be valid.
I am learning to make tests for a ruby on rails application, and I appear to have run into an issue. I am trying to login a user as I do not have access to the session[:user_id] inside these two tests. So I have made a method inside the test_helper that defines these methods in my create_categories_test, and they run, but when I set a login for them, it returns this error:
(byebug) post login_path session: [{username: user.username, password: password}]
*** NoMethodError Exception: undefined method `[]' for nil:NilClass
This is my helper method in test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
def sign_in_as(user, password)
post login_path, session: {username: user.username, password: password}
end
end
Note: I put a debugger inside my method and ran the same line of code which returned nil(for some reason)
Here's my code for my create_categories_test.rb
def setup
#user = User.create(username: "John", email: "john#doe.com", password: "password", admin: true)
end
test "get new category form and create category" do
sign_in_as(#user, "password")
get new_category_path
assert_template 'categories/new'
assert_difference 'Category.count' do
post_via_redirect categories_path, category: {name: "sports"}
end
assert_template 'categories/index'
assert_match "sports", response.body
end
test "invalid category submission results in failure" do
sign_in_as(#user, "password")
get new_category_path
assert_template 'categories/new'
assert_no_difference 'Category.count', 1 do
post categories_path, category: {name: " "}
end
assert_template 'categories/new'
assert_select 'h2.panel-title'
assert_select 'div.panel-body'
end
My login controller:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(username: params[:sessions][:username])
if user && user.authenticate(params[:sessions][:password])
session[:user_id] = user.id
flash[:success] = "You have successfully logged in"
redirect_to user_path(user)
else
flash.now[:danger] = "There was something wrong with your login details"
render 'new'
end
end
def destroy
session[:user_id] = nil
flash[:success] = "You have successfully logged out"
redirect_to root_path
end
end
I assume the problem in your params in the post method:
post login_path session: [{username: user.username, password: password}]
You post an array [{username: user.username, password: password}], but controller expect an hash:
post login_path session: {username: user.username, password: password}
Also your login helper:
#session: {}
post login_path, session: {username: user.username, password: password}
^^^^^^^
But controller expect:
#sessions: {}
user = User.find_by(username: params[:sessions][:username])
^^^^^^^^
Probably this is off-topic question, because it's about a simple typo in the code.
Here is my Rspec when testing an API end point related to Users:
context "updating a user" do
let(:user) { User.create! }
it "should let me update a user without an email" do
put "/api/v1/users/#{user.id}", {:user => {:first_name => 'Willy'}}.to_json, {'CONTENT_TYPE' => 'application/json', 'HTTP_AUTHORIZATION' => "Token token=\"#{auth_token.access_token}\""}
p user.inspect
end
And the controller action that I am testing looks like this:
def update
begin
#user = User.find(params[:id])
if #user.update_attributes(params[:user])
p #user.inspect
render json: #user, :except => [:created_at, :updated_at]
else
render json: { :errors => #user.errors }, :status => :unprocessable_entity
end
rescue ActiveRecord::RecordNotFound
head :not_found
end
end
Surprisingly, the #user.inspect in the controller shows this:
"#<User id: 2, first_name: \"Willy\", last_name: nil, email: nil, state: nil, created_at: \"2013-06-22 11:21:22\", updated_at: \"2013-06-22 11:21:22\">"
And the user.inspect in the rspec, right after the call to the controller has been done, looks like this:
"#<User id: 2, first_name: nil, last_name: nil, email: nil, state: nil, created_at: \"2013-06-22 11:21:22\", updated_at: \"2013-06-22 11:21:22\">"
Why does the Rspec not catch the updates? I mean, I have tested this manually and the database gets updated correctly.
What am I missing here?
In rspec example you define user method with let, which returns ActiveRecord object. Your controller is creating different object, that points to the same database entry. Change in db is not reflected in user object in rspec example, as there is no callback mechanism that would notify it to change.
Using #reload method on AR object in test should solve your problem, as it forces reloading data from db.