I'm having a problem where I am doing a query across tables in Rails, and Rails is returning an error saying that I am sending an argument.
Here's the controller code:
def destroy
#version = Version.where(number: params[:number]).joins(:packages).where(packages: {name: params[:name]}).all
if #version.destroy
render json: {}, status: 204
else
render json: { "error": "Version could not be deleted." }, status: 422
end
end
Here's the failing test:
describe "DELETE #destroy" do
context "valid parameters" do
before { #package = FactoryGirl.create(:package) }
before { #version = FactoryGirl.create(:version) }
it "deletes the version" do
expect {
delete :destroy, format: :json, access_token: #token.token, name: #package.name, number: #version.number
}.to change(Version, :count).by(-1)
end
it "returns 204" do
delete :destroy, format: :json, access_token: #token.token, name: #package.name, number: #version.number
response.status.should eq(204)
end
end
end
Here's the error message:
Failure/Error: delete :destroy, format: :json, access_token: #token.token, name: #package.name, number: #version.number
ArgumentError:
wrong number of arguments (0 for 1)
# ./app/controllers/api/v0/versions_controller.rb:19:in `destroy'
You set #version to be collection (ActiveRecord::Relation) instead of singular record instance. destroy on collections takes argument. If you want to destroy every record in this collection, you can use destroy_all:
#versions.destroy_all
Note: I renamed your instance variable to plural form, so it isn't misleading now.
If you want to find just one Version and destroy it, you can do:
#version = Version.joins(:packages).where(packages: {name: params[:name]}).find_by(number: params[:number])
Related
I am attempting to test returning errors on a non-unique attempt at creating a user in this API.
RSpec.describe 'POST /signup', type: :request do
let(:url) { '/signup' }
let(:params) do
{
user: {
email: 'user#example.com',
password: 'password'
}
}
end
context 'when user is unauthenticated' do
before { post url, params: params }
it 'returns 200' do
expect(response.status).to eq 200
end
it 'returns a new user' do
expect(response).to match_response_schema('user')
end
end
context 'when user already exists' do
before do
Fabricate :user, email: params[:user][:email]
post url, params: params
end
it 'returns bad request status' do
expect(response.status).to eq 400
end
it 'returns validation errors' do
expect(json['errors'].first['title']).to eq('Bad Request')
end
end
end
Above is my spec file. Below is the registration file that is throwing the error:
class RegistrationsController < Devise::RegistrationsController
respond_to :json
def create
build_resource(sign_up_params)
resource.save
render_resource(resource)
end
end
The error that I am getting:
3) POST /signup when user already exists returns validation errors
Failure/Error: resource.save
ActiveRecord::RecordNotUnique:
SQLite3::ConstraintException: UNIQUE constraint failed: users.email:
INSERT INTO "users" ("email", "encrypted_password") VALUES (?, ?)
I know that it is going to throw that error, that's the point I've defined the following in my application controller to return the correctly formatted error:
class ApplicationController < ActionController::API
def render_resource(resource)
if resource.errors.empty?
render json: resource
else
validation_error(resource)
end
end
def validation_error(resource)
render json: {
errors: [
{
status: '400',
title: 'Bad Request',
detail: resource.errors,
code: '100'
}
]
}, status: :bad_request
end
end
The goal is that if resource.save doesn't save, or throws and error that the render_resource function will return a JSON formatted error. However, every time it hits the resource.save it throws the error in my test and stops the rest of the test. Any help would be greatly appreciated!
save returns false and populates errors when a record wasn't saved to the database because of failing validations. In your case, there aren't failing any validations but the database raised an exception. These kinds of errors aren't handled in your code.
A unique index in the database is important to really ensure that you do not end up with duplicates in your database, but you still should add a validation to your model to show nice error messages to the user.
# add to app/models/user.rb
validates :email, uniqueness: { case_sensitive: false }
Read more about this type of validation in the Rails Guides.
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'm trying to get a create action to set up properly.
I keep getting an error: ArgumentError: Unknown keyword: topic
Here is the testing:
require 'rails_helper'
RSpec.describe TopicsController, type: :controller do
let(:my_topic) { Topic.create!(name: RandomData.random_sentence, description: RandomData.random_paragraph)}
describe "POST create" do
it "increases the number of topics by 1" do
expect{ post :create, {topic: {name: RandomData.random_sentence, description: RandomData.random_paragraph}}}.to change(Topic,:count).by(1)
end
it "assigns Topic.last to #topic" do
post :create, { topic: {name: RandomData.random_sentence, description: RandomData.random_paragraph}}
expect(assigns(:topic)).to eq Topic.last
end
it "redirects to the new topic" do
post :create, {topic: {name: RandomData.random_sentence, description: RandomData.random_paragraph}}
expect(response).to redirect_to Topic.last
end
end
Here is the controller:
def create
#topic = Topic.new
#topic.name = params[:topic][:name]
#topic.description = params[:topic][:description]
#topic.public = params[:topic][:public]
if #topic.save
redirect_to #topic, notice: "Topic was saved successfully."
else
flash.now[:alert] = "Error creating topic. Please try again"
render :new
end
end
I'm trying to figure out what I'm missing that is causing this error I've been staring at it for hours and have tried to edit it multiple times to no avail. I can't figure it out. The rest of the project I've been working on has been okay however I cannot figure out why I can't get the word topic to convert successfully. Thanks for taking a look.
Replace :topic with :params. That's the expected keyword for your test. It is already clear to RSpec that you're testing for Topic since your spec file is TopicsController.
The problem is that the post method takes keyword arguments as a second argument.
If you need to specify params, the params keyword should be used:
post :create, params: { topic: { name: ..., description: ... } }
I have been tearing my hair trying to make the test to pass. I have a JSON API that looks like this:
{
"data": [
{
"id": "b99f8173-0492-457f-9de9-6c1d8d6832ed",
"type": "manufacturer_organizations",
"attributes": {
"account_number": "random test 123"
},
"relationships": {
"organization": {
"data": {
"id": "fb20ddc9-a3ee-47c3-bdd2-f710541ff89c",
"type": "organizations"
}
},
"manufacturer": {
"data": {
"id": "1",
"type": "manufacturers"
}
}
}
},...
I am trying to make a post :create test in rails.
let!(:manufacturer_organization) {FactoryGirl.create(:manufacturer_organization)}
let(:manufacturer_organization2) { FactoryGirl.create(:manufacturer_organization)}
...
describe "POST create" do
it "posts a valid manufacturer organization data" do
authenticate
organization = FactoryGirl.create(:organization)
manufacturer = FactoryGirl.create(:manufacturer)
post :create, manufacturer_organization2.to_json #what should I put here instead?
expect(json['data'].length).to eq(2)
end
#=> error: JSON::ParserError: A JSON text must at least contain two octets!
I have tried various SO posts (this), this, and this article
Here are some of the attempts I have tried:
post :create, params: {organization_id: organization.id, manufacturer: manufacturer.id, account_number: "123 test account number"}
#=> error: JSON::ParserError:
A JSON text must at least contain two octets!
or
post :create, params: :manufacturer_organization_2
#=>
NoMethodError:
undefined method `symbolize_keys' for :manufacturer_organization_2:Symbol
or
json = { :format => 'json', :manufacturer_organization => { :account_number => "foo123", :organization_id => organization.id, :manufacturer_id => manufacturer.id } }
post :create, json
#=> NoMethodError:
undefined method `length' for nil:NilClass
How can I test my controller to accept manufacturer_id, organization_id, and account_number via post :create? Right now the way I test it is to count initial json['data'].length (initially 1); at the end I expect json['data'].length to be 2 after successful post :create. How can I mock creating a valid manufacturer_organization input?
Edit:
Sorry, forgot to put my json method helper:
def json
JSON.parse(response.body)
end
Also, this pass:
describe "POST create" do
it "posts a valid manufacturer organization data" do
authenticate
organization = FactoryGirl.create(:organization)
manufacturer = FactoryGirl.create(:manufacturer)
post :create, {account_number: "Test account numba", organization_id: organization.id, manufacturer_id: manufacturer.id}
expect(response).to be_success
end
while adding expect(json['success']).to eq("Yay!") gives me this error:
JSON::ParserError:
A JSON text must at least contain two octets!
Controller:
def create
#manufacturer_organization = ManufacturerOrganization.new(manufacturer_organization_params)
if #manufacturer_organization.save
puts "success!"
render json: {success: "Yay!"}
else
puts "Sorry, something went wrong!"
end
end
def manufacturer_organization_params
api_params.permit(:organization_id, :manufacturer_id, :account_number)
end
while #api_params ||= ActionController::Parameters.new(ActiveModelSerializers::Deserialization.jsonapi_parse(params))
In RSpec you never* need to explicitly format the params.
post :create, params: { foo: 'bar' }, format: :json
This will properly format the hash { foo: 'bar' } as JSON in the request body.
To create a hash which matches the JSONAPI.org structure you can create a helper:
# support/api_spec_helper.rb
module APISpecHelper
def to_json_api(model)
{
data: {
type: ActiveModel::Naming.plural(model),
attributes: model.attributes
}.tap do |hash|
hash[:id] = model.id if model.persisted?
end
}
end
end
You can also use the JSONAPI-RB gem or ActiveModel::Serializers to constuct/deconstruct JSONAPI responses/params.
require 'rails_helper'
require 'api_spec_helper'
RSpec.request "Manufacturer organizations" do
include APISpecHelper
describe "POST '/manufacturer_organizations'" do
let(:valid_params) do
to_json_api(FactoryGirl.build(:manufacturer_organization))
end
let(:invalid_params) do
to_json_api(ManufacturerOrganization.new(
foo: 'bad_value'
))
end
describe "with valid attributes" do
it "creates a manufacturer organization" do
expect do
post '/manufacturer_organizations', params: valid_params, format: :json
end.to change(ManufacturerOrganization, :count).by(+1)
end
it "has the correct response" do
post '/manufacturer_organizations', params: valid_params, format: :json
expect(response).to have_status :created
expect(response.headers[:location]).to eq(
manufacturer_organization_path(ManufacturerOrganization.last)
)
end
end
describe "with valid attributes" do
it "does not create a manufacturer organization" do
expect do
post '/manufacturer_organizations', params: invalid_params, format: :json
end.to_not change(ManufacturerOrganization, :count)
end
it "has the correct response" do
post '/manufacturer_organizations', params: invalid_params, format: :json
expect(response).to have_status :unproccessable_entity
end
end
end
end
Returning the correct status codes.
Returning the correct response codes is pretty simple:
def create
#resource = Resource.create(some_params)
if #resource.save
# you can respond by pointing at the newly created resource but with no body
head :created, location: #resource
# or
render json: #resource,
status: :created,
location: #resource
else
render json: { errors: #resource.errors.full_messages },
status: :unprocessable_entity
end
end
If a POST request did not include a Client-Generated ID and the
requested resource has been created successfully, the server MUST
return a 201 Created status code.
http://jsonapi.org/format/#crud
Other Responses
A server MAY respond with other HTTP status codes.
A server MAY include error details with error responses.
The commonly accepted practice is to use 422 - Unprocessable Entity for validation errors.
One small concern is that you should use a serializer to give the correct JSON response and also serialize the correct error objects.
I'm trying to do some integration tests with rspec on rails 4 but I alway get a "ActionController::UnknownFormat" exception when running the tests.
I tried two different ways:
Failure/Error: post sensors_path, sensor: #sensor_attributes.to_json
ActionController::UnknownFormat:
ActionController::UnknownFormat
Failure/Error: post sensors_path, sensor: #sensor_attributes, format: :js
ActionController::UnknownFormat:
ActionController::UnknownFormat
Here is the rspec code:
it "should change the number of sensors" do
lambda do
post sensors_path, sensor: #sensor_attributes.to_json
end.should change(Sensor, :count).by(1)
end
it "should be successful" do
post sensors_path, sensor: #sensor_attributes, format: :js
response.should be_success
end
And this is the create statement of the controller:
def create
respond_to do |format|
format.json do
#sensor = Sensor.new(params["sensor"])
#sensor.uuid = SecureRandom.uuid
#sensor.save
render nothing: true
end
end
end
And the sensor_attributes:
before do
#sensor_attributes = { name: "Testname", description: "This is a Test-Description." }
end
And the routes:
resources :sensors
Any idea what went wrong?
You're using json format in controller, but you're passing format: :js in the test.
It should be:
post sensors_path, sensor: #sensor_attributes, format: :json
This is for future readers.
Somehow the answer did not help me.
Writing like following worked for me -
params = {
sensor: {
name: "Testname",
description: "This is a Test-Description."
},
format: :json
}
post sensors_path, params
Appending .json to the path worked for me as well (since I was using a shared rspec example and could not just append format: json)...
post "#{sensors_path}.json", sensor: #sensor_attributes
it "creates a new sensor" do
expect {
post :create, {:sensor => {here_you_need_to_put_the_variables_which_are_needed_to_create_a_sensor}}
}.to change(Sensor, :count).by(1)
end
In you create method, the following line
#sensor = Sensor.new(params["sensor"])
should be:
#sensor = Sensor.new(params[:sensor])
I don't know the structure of you code. All the solution code i gave is based on my own assumption.