So I'm trying to test a post request in order to save some book details. The response comes as raw JSON due to its being stringified in the client using formData. So that I can format the response appropriately in the controller.
I cannot find any clear way to send raw JSON parameters as rails automatically coerce these parameters as a HASH. Any suggestion?
Rails 5.1.4
Rspec 3.7.2
books_spec.rb
# Test suite for POST /books
describe 'POST /books' do
# valid payload
let(:valid_attributes) do
# send stringify json payload
{
"title": "Learn Elm",
"description": "Some good",
"price": 10.20,
"released_on": Time.now,
"user_id": user.id,
"image": "example.jpg"
}.to_json
end
# no implicit conversion of ActiveSupport::HashWithIndifferentAccess into String
context 'when the request is valid' do
before { post '/books', params: valid_attributes, headers: headers }
it 'creates a books' do
expect(json['book']['title']).to eq('Learn Elm')
end
it 'returns status code 201' do
expect(response).to have_http_status(201)
end
end
context 'when the request is invalid' do
let(:valid_attributes) { { title: nil, description: nil }.to_json }
before { post '/books', params: valid_attributes, headers: headers }
it 'returns status code 422' do
expect(response).to have_http_status(422)
end
it 'returns a validation failure message' do
expect(response.body)
.to match(/Validation failed: Title can't be blank, Description can't be blank/)
end
end
end
books_controller.rb
# POST /books
def create
#book = current_user.books.create!(book_params)
render json: #book, status: :created
end
def book_params
binding.pry # request.params[:book] = HASH
parsed = JSON.parse(request.params[:book])
params = ActionController::Parameters.new(parsed)
params['image'] = request.params[:image]
params.permit(
:title,
:description,
:image,
:price,
:released_on,
:user
)
end
#you should try test the request first like this and if you see this helping you I will send you the 200 test
describe "#create" do
context "when the request format is not json" do
let(:error_message) do
{ "Invalid Request Format" => "Request format should be json" }
end
before do
post :create, headers: { "CONTENT_TYPE": "XML" }
end
it "should return a status of 400" do
expect(response).to have_http_status(400)
end
it "should return an invalid request format in the body" do
expect(json(response.body)).to eql error_message
end
end
end
Related
I'm quite having an issue on my Rspec. So basically what I want is to send a request to the API, and have a proper response. When I send the data to the API endpoint, and I puts what it sends, it looks like it sends all the data correctly, but at the response level, it says that I am not sending the required parameters. (I am also using VCR to test)
My RSPEC:
require 'rails_helper'
describe 'Create Vendor', type: :request do
let(:user) { create(:alter_user) }
let(:vendor_attributes) do
{
#These are all the required parameters
vendor: {
token: '',
refcode: '',
request_type: 'renters',
first_name: 'Tony',
last_name: 'Stark',
phone: '6504881234',
phone_type: 'mobile',
email: 'tony#starkindustries.com',
address1: '123 Main Street',
address_city: 'Palo Alto',
address_state: 'CA',
address_zip: '94301',
DOB: '1982-10-27'
}
}
end
after do
VCR.eject_cassette
end
it 'creates a vendor' do
VCR.use_cassette('create_vendor') do
post '/api/v2/vendor', params: vendor_attributes.to_json, headers: {'Content-Type' => 'application/json', Authorization: "Token token=#{user.key}"}
response_body = JSON.parse(response.body)
expect(response_body['refcode']).to eq('corp-api-test')
end
end
it 'fails if the vendor is missing attributes' do
VCR.use_cassette('create_vendor_missing_attributes') do
post '/api/v2/vendor', params: {vendor: {last_name: 'Stark', phone: '6504881234'}}.to_json, headers: {'Content-Type' => 'application/json', Authorization: "Token token=#{user.key}"}
expect(response.status).to eq(422)
end
end
it 'requires Authorization token header' do
post '/api/v2/vendor', params: vendor_attributes.to_json, headers: {'Content-Type' => 'application/json'}
expect(response.status).to eq(401)
end
end
When I execute the rspec filePath I receive this:
Create Vendor
Post Request----: {"request_type":"renters","first_name":"Tony","last_name":"Stark","phone":"6504881234","phone_type":"mobile","email":"tony#starkindustries.com","address1":"123 Main Street","address_city":"Palo Alto","address_state":"CA","address_zip":"94301","DOB":"1982-10-27","token":"6ed469496be882f0163c453627fbcac15b773733db123a7c2faf68f42ea7586a","refcode":"corp-api-test"}
-----------------------------------------------------
Response----: {:action=>"new_vendor", :user_hash=>"e59871c35edb7f622c97acfcc20955e9", :result=>"success", :agent_phone=>"888-888-8888", :vendor_uuid=>"242992518092902061", :policy_shell_uuids=>{"RENT"=>"243548182003445420"}, :marketing_headline=>"Test Marketing Headline", :marketing_content=>"<ul><li>Test Marketing Content</li></ul>"}
-----------------------------------------------------
It was successful
creates a vendor (FAILED - 1)
Post Request----: {"last_name":"Stark","phone":"6504881234","token":"6ed469496be882f0163c453627fbcac15b773733db123a7c2faf68f42ea7586a","refcode":"corp-api-test"}
-----------------------------------------------------
Response----: {:result=>"error", :message=>"missing fields: first_name is a required field, email is a required field, address1 is a required field, address_city is a required field, address_state is a required field, address_zip is a required field, DOB is a required field, phone_type is a required field"}
-----------------------------------------------------
fails if the vendor is missing attributes
requires Authorization token header
Failures:
1) Create Vendor creates a vendor
Failure/Error: response_body = JSON.parse(response.body)
JSON::ParserError:
809: unexpected token at ''
# ./spec/requests/create_vendor_spec.rb:34:in `block (3 levels) in <top (required)>'
# ./spec/requests/create_vendor_spec.rb:32:in `block (2 levels) in <top (required)>'
# ./spec/spec_helper.rb:85:in `block (3 levels) in <top (required)>'
# ./spec/spec_helper.rb:84:in `block (2 levels) in <top (required)>'
This is my vendor controller
As you can see here I print what I send to the API, it sends the data but then it fails to create a vendor.
def corp_new_vendor_v2
arguments = create_vendor_arguments
https = Net::HTTP.new(VENDOR_CREATE_VENDOR_URI.host, VENDOR_CREATE_VENDOR_URI.port)
https.use_ssl = true
post_request = Net::HTTP::Post.new(VENDOR_CREATE_VENDOR_URI)
post_request['Content-Type'] = 'application/json'
post_request.body = JSON.dump(arguments)
print "Post Request----: #{post_request.body}\n\n"
print "-----------------------------------------------------\n"
response = JSON.parse(https.request(post_request).read_body).symbolize_keys
print "Response----: #{response}\n\n"
print "-----------------------------------------------------\n"
if response[:result] == 'success'
puts 'It was successful'
else
render json: { error: response[:message] }, status: :unprocessable_entity
end
end
# This is used once we touch the new vendor endpoint, and response with a response
def create_vendor_attributes(response)
response.slice(:action, :user_hash, :result, :vendor_uuid, :policy_shell_uuids, :refcode, :return_URL)
end
def create_vendor_params
params.require(:vendor).permit(
:request_type, :first_name, :last_name, :phone, :phone_type, :email, :address1, :address_city, :address_state, :address_zip, :DOB
)
end
def create_vendor_arguments
create_vendor_params.to_h.merge(token: CORP_RENTER_TOKEN, refcode: CORP_REFCODE)
end
end
What am I missing here? On my spec I'm sending everything but I keep receiving the par JSON error.
I will appreciate the help!
EDIT
binding.pry
Create Vendor
# here it sends all the data as in the unit testing
Post Request (Unit Test)----: {"request_type":"renters","first_name":"Tony","last_name":"Stark","phone":"6504881234","phone_type":"mobile","email":"tony#starkindustries.com","address1":"123 Main Street","address_city":"Palo Alto","address_state":"CA","address_zip":"94301","DOB":"1982-10-27","token":"6ed469496be882f0163c453627fbcac15b773733db123a7c2faf68f42ea7586a","refcode":"corp-api-test"}
-----------------------------------------------------
# And here it responds correctly
Response----(Unit Test): {:action=>"new_vendor", :user_hash=>"e59871c35edb7f622c97acfcc20955e9", :result=>"success", :agent_phone=>"888-888-8888", :vendor_uuid=>"242992518092902061", :policy_shell_uuids=>{"RENT"=>"243548182003445420"}, :marketing_headline=>"Test Marketing Headline", :marketing_content=>"<ul><li>Test Marketing Content</li></ul>"}
-----------------------------------------------------
creates a vendor (FAILED - 1)
# Here for some reason it only sends two parameters
Post Request----(Not Unit Test): {"last_name":"Stark","phone":"6504881234","token":"6ed469496be882f0163c453627fbcac15b773733db123a7c2faf68f42ea7586a","refcode":"corp-api-test"}
-----------------------------------------------------
# And obviously the server response with an error cause it's not sending all the required data.
Response----(Not Unit Test): {:result=>"error", :message=>"missing fields: first_name is a required field, email is a required field, address1 is a required field, address_city is a required field, address_state is a required field, address_zip is a required field, DOB is a required field, phone_type is a required field"}
create method
def create
vendor_new_vendor_v2
end
which uses these two methods to sends the parameters
def create_vendor_params
params.require(:vendor).permit(
:request_type, :first_name, :last_name, :phone, :phone_type, :email, :address1, :address_city, :address_state, :address_zip, :DOB
)
end
def create_vendor_arguments
create_vendor_params.to_h.merge(token: CORP_RENTER_TOKEN, refcode: CORP_REFCODE)
end
specs
it 'creates a vendor' do
VCR.use_cassette('create_vendor') do
post '/api/v2/vendor', params: vendor_attributes.to_json, headers: {'Content-Type' => 'application/json', Authorization: "Token token=#{user.key}"}
response_body = JSON.parse(response.body)
expect(response_body['refcode']).to eq('corp-api-test')
end
end
#I just saw that these two `last_name`and `phone`are being printed on the puts I did above.
it 'fails if the vendor is missing attributes' do
VCR.use_cassette('create_vendor_missing_attributes') do
post '/api/v2/vendor', params: {vendor: {last_name: 'Stark', phone: '6504881234'}}.to_json, headers: {'Content-Type' => 'application/json', Authorization: "Token token=#{user.key}"}
expect(response.status).to eq(422)
end
end
it 'requires Authorization token header' do
post '/api/v2/vendor', params: vendor_attributes.to_json, headers: {'Content-Type' => 'application/json'}
expect(response.status).to eq(401)
end
end
You are missing the render :json for the success.
Update your controller with the following
if response[:result] == 'success'
render json: response, status: :ok
else
render json: { error: response[:message] }, status: :unprocessable_entity
end
I have simple action show
def show
#field = Field.find_by(params[:id])
end
and i want write spec for it
require 'spec_helper'
RSpec.describe FieldsController, type: :controller do
let(:field) { create(:field) }
it 'should show field' do
get :show, id: field
expect(response.status).to eq(200)
end
end
but I have got an error
Failure/Error: get :show, id: field
ArgumentError:
unknown keyword: id
How to fix it?
HTTP request methods will accept only the following keyword arguments
params, headers, env, xhr, format
According to the new API, you should use keyword arguments, params in this case:
it 'should show field' do
get :show, params: { id: field.id }
expect(response.status).to eq(200)
end
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 have a Rails 5 API only app and using knock to do JWT authenticate.
After complete the model and model spec, I start to do the request spec.
But I have no idea how to complete the authentication inside the request spec in the right way,
My users controller,
module V1
class UsersController < ApplicationController
before_action :authenticate_user, except: [:create]
end
end
Application controller,
class ApplicationController < ActionController::API
include Knock::Authenticable
include ActionController::Serialization
end
My stupidest solution (call the get token request to get the JWT before the rest request),
context 'when the request contains an authentication header' do
it 'should return the user info' do
user = create(:user)
post '/user_token', params: {"auth": {"email": user.email, "password": user.password }}
body = response.body
puts body # {"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0ODgxMDgxMDYsInN1YiI6MX0.GDBHPzbivclJfwSTswXhDkV0TCFCybJFDrjBnLIfN3Q"}
# use the retrieved JWT for future requests
end
end
Any advice is appreciated.
def authenticated_header(user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
{ 'Authorization': "Bearer #{token}" }
end
describe 'GET /users?me=true' do
URL = '/v1/users?me=true'
AUTH_URL = '/user_token'
context 'when the request with NO authentication header' do
it 'should return unauth for retrieve current user info before login' do
get URL
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the request contains an authentication header' do
it 'should return the user info' do
user = create(:user)
get URL, headers: authenticated_header(user)
puts response.body
end
end
end
With the help of Lorem's answer, I was able to implement something similar for my request spec. Sharing it here for others to see an alternate implementation.
# spec/requests/locations_spec.rb
require 'rails_helper'
RSpec.describe 'Locations API' do
let!(:user) { create(:user) }
let!(:locations) { create_list(:location, 10, user_id: user.id) }
describe 'GET /locations' do
it 'reponds with invalid request without JWT' do
get '/locations'
expect(response).to have_http_status(401)
expect(response.body).to match(/Invalid Request/)
end
it 'responds with JSON with JWT' do
jwt = confirm_and_login_user(user)
get '/locations', headers: { "Authorization" => "Bearer #{jwt}" }
expect(response).to have_http_status(200)
expect(json.size).to eq(10)
end
end
end
confirm_and_login_user(user) is defined in a request_spec_helper which is included as a module in rails_helper.rb:
# spec/support/request_spec_helper.rb
module RequestSpecHelper
def json
JSON.parse(response.body)
end
def confirm_and_login_user(user)
get '/users/confirm', params: {token: user.confirmation_token}
post '/users/login', params: {email: user.email, password: 'password'}
return json['auth_token']
end
end
I'm using the jwt gem for generating my tokens as described in this SitePoint tutorial (https://www.sitepoint.com/introduction-to-using-jwt-in-rails/)
Lorem's answer mostly worked for me. I got unrecognized keyword setting headers: on the get. I modified the authenticated_header method and put it in support/api_helper.rb so I could reuse it. The modification is to merge the auth token into request.headers.
# spec/support/api_helper.rb
module ApiHelper
def authenticated_header(request, user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
request.headers.merge!('Authorization': "Bearer #{token}")
end
end
In each spec file testing the api, I include api_helper.rb. And I call authenticated_header just before the get statement when testing the case of valid authentication...
# spec/controllers/api/v2/search_controller_spec.rb
RSpec.describe API::V2::SearchController, type: :controller do
include ApiHelper
...
describe '#search_by_id' do
context 'with an unauthenticated user' do
it 'returns unauthorized' do
get :search_by_id, params: { "id" : "123" }
expect(response).to be_unauthorized
end
end
context 'with an authenticated user' do
let(:user) { create(:user) }
it 'renders json listing resource with id' do
expected_result = { id: 123, title: 'Resource 123' }
authenticated_header(request, user)
get :search_by_id, params: { "id" : "123" }
expect(response).to be_successful
expect(JSON.parse(response.body)).to eq expected_result
end
end
The key lines in this second test are...
authenticated_header(request, user)
get :search_by_id, params: { "id" : "123" }
Writing specs for a json api the routes are defaulted to only accept json requests like so:
Rails.application.routes.draw do
namespace :api, default: { format: 'json' } do
namespace :v1 do
resources :users, only: [:create]
end
end
end
I keep getting the following error:
Failure/Error: post :create, json
ActionController::UrlGenerationError:
No route matches {:action=>"create", :company_name=>"Wilderman, Casper and Medhurst", :controller=>"api/v1/users", :email=>"lillie_prohaska#example.com", :password=>"difdcbum5q", :username=>"gielle"}
Traditionally to get around this error I have set the CONTENT_TYPE and the HTTP_ACCEPT on the request so that it will pass the json formatting requirement.
My specs are written like so:
describe Api::V1::UsersController do
before :each do
#request.env['HTTP_ACCEPT'] = 'application/json'
#request.env['CONTENT_TYPE'] = 'application/json'
end
describe "POST#create" do
context "with valid attirbutes" do
let(:json) { attributes_for(:user) }
it "creates a new user" do
expect{ post :create, json }.to change{ User.count }.by(1)
end
it "returns status code 200" do
post :create, json
expect(response.status).to be(200)
end
it "should contain an appropriate json response" do
post :create, json
user = User.last
json_response = {
"success" => true,
"id" => user.id.to_s,
"auth_token" => user.auth_token
}
expect(JSON.parse(response.body)).to eq (json_response)
end
end
end
end
I have also tried adding a hash with { format: 'json' } which has also failed me.
According to the comments on the accepted answer for the question below setting the request environment headers will no longer work with rspec 3:
Set Rspec default GET request format to JSON
How would this be achieved in rspec 3 in Rails 4.1.1?
Thanks!
Here is how I do it over here:
[:xml, :json].each do |format|
describe "with #{format} requests" do
let(:api_request) { { :format => format } }
describe "GET 'index'" do
before :each do
api_request.merge attributes_for(:user)
end
it 'returns HTTP success' do
get :index, api_request
expect(response).to be_success
end
end
end