I'm trying to create an integration testing for the creating of a record called books. I'm having problems creating the hash in the tests. This is my code:
test/integration/creating_book_test.rb
require 'test_helper'
class CreatingBookTest < ActionDispatch::IntegrationTest
def setup
#michael_lewis = Author.create!(name: 'Michael Lewis')
#business = Genre.create!(name: 'Business')
#sports = Genre.create!(name: 'Sports')
#analytics = Genre.create!(name: 'Analytics')
end
test "book is created successfully" do
post '/api/books', { book: book_attributes }.to_json, {
'Accept' => 'application/json',
'Content-Type' => 'application/json'
}
... assertions...
end
def book_attributes
{title: 'Moneyball',
year: 2003,
review: 'Lorem Ipsum',
rating: 5,
amazon_id: '10832u13kjag',
author_ids: [#michael_lewis.id],
genre_ids: [#business.id, #sports.id, #analytics.id]
}
end
end
In the controller, I'm whitelisting the params with:
def book_params
params.require(:book).permit(:title, :year, :review, :rating, :amazon_id, :author_ids, :genre_ids)
end
The problem is that I'm not receiving :author_ids and :genre_ids in the controller. It seems like arrays don't get sent to the controller, so I can't test that associations work like they should.
Thanks.
You strong paramter declaration is wrong. Here is the fix:
params.require(:book).permit(:title, :year, :review, :rating, :amazon_id, author_ids: [], genre_ids: [])
From Permitted Scalar Values documentation :
..To declare that the value in params must be an array of permitted scalar values map the key to an empty array.
Related
I have the following hash in my controller.
#order = {
:id => "somestringid",
:user_id => "someotherstringid",
:amount => 19.99,
:metadata => [
{
:type => :shipping_data,
:address => "line 1 of address, line 2 of address, city, state, pincode"
},
{
:type => :payment,
:stripe_customer_id => "somestringid",
:stripe_card_id => "someotherstringid"
},
{
:type => :contact,
:email => "someone#example.com",
:phone => "1231231231"
}
]
}
Notes:
The "metadata" is a list of objects.
There can be 0, 1 or more metadata objects.
Each metadata object has a different structure.
The only common key for all metadata objects is the "type" key.
I want to use rabl to generate the following json, but cannot figure out what I should put into my template.
The JSON output that I want should look like the following.
{
"id": "somestringid",
"user_id": "someotherstringid",
"amount": 19.99,
"metadata": [
{
"type": "shipping_data",
"address": "line 1 of address, line 2 of address, city, state, pincode"
},
{
"type": "payment",
"stripe_customer_id": "somestringid",
"stripe_card_id": "someotherstringid"
},
{
"type": "contact",
"email": "someone#example.com",
"phone": "1231231231"
}
]
}
What should I put into my template, so that I get the desired output?
Note
I am not sure whether by rabl you mean you are using standard rabl gem OR rabl-rails because you haven't provided any details about the nature of your application and it is built using what all libraries.
But my solution below is in context of a Rails application using rabl-rails gem. And the rabl-rails gem's README says following:
rabl-rails is faster and uses less memory than the standard rabl gem while letting you access the same features. There are some slight changes to do on your templates to get this gem to work but it should't take you more than 5 minutes.
So I guess it should not be a problem to adapt this solution in context of standard rabl gem whether the application is built Rails or Non-Rails based. My aim is to provide a guidance on the approach which can be used to achieve your desired output.
Now coming to the solution approach:
Using some abstractions you can design a flexible and maintainable solution. Let me elaborate:
Assuming you have a plain ruby-class Order like following or if you don't have any such class you can define it easily using virtus gem which provides some handy out-of-the-box features for a class:
app/models/order.rb
class Order
attr_accessor :id, :user_id, :amount, :order_metadata_obj_arr
....
..
end
app/models/order_metadata.rb
class OrderMetadata
attr_accessor :metadata_type, :metadata_data_obj
...
...
end
app/models/shipping_data.rb
class ShippingData
attr_accessor :address
end
app/models/payment_data.rb
class PaymentData
attr_accessor :stripe_customer_id, :stripe_card_id
end
app/models/contact_data.rb
class ContactData
attr_accessor :email, :phone
end
/app/json_views/orders/metadata/shipping.rabl
attribute :address
/app/json_views/orders/metadata/payment.rabl
attribute :stripe_customer_id, :stripe_card_id
/app/json_views/orders/metadata/contact.rabl
attribute :email, :phone
/app/json_views/orders/order_metadata.rabl
attribute :type
# Anonymous node.
node do |order_metadata_obj|
template_path = "./orders/metadata/#{order_metadata_obj.type}"
metadata_data_obj = order_metadata_obj.metadata_data_obj
partial(template_path, object: metadata_data_obj)
end
/app/json_views/order.rabl
attributes :id, :user_id, :amount
node(:metadata) do |order_obj|
partial('./orders/order_metadata', object: order_obj.order_metadata_obj_arr)
end
Then in your controller do this
/app/controllers/my_controller.rb
class MyController < MyBaseController
def my_action
order_obj = create_order_obj_from_hash(order_hash)
order_json = order_json_representation(order_obj)
status = 200
render json: order_json, status: status
end
private
def create_order_obj_from_hash(order_hash)
order_metadata_arr = order_hash[:metadata]
order_metadata_obj_arr = []
order_metadata_arr.each do |item_hash|
md_type = item_hash[:type]
md_obj = case md_type
when :shipping_data
ShippingData.new(address: item_hash[:address])
when :payment
PaymentData.new(stripe_customer_id: item_hash[:stripe_customer_id], stripe_card_id: item_hash[:stripe_card_id])
when :contact
ContactData.new(email: item_hash[:email], phone: item_hash[:phone])
else
// unsupported md_type
end
unless md_obj.nil?
omd_obj = OrderMetadata.new(metadata_type: md_type, metadata_data_obj: md_obj)
order_metadata_obj_arr << omd_obj
end
end
order_attrs = order_hash.slice(:id, :user_id, :amount)
order = Order.new(order_attrs)
order.order_metadata_obj_arr = order_metadata_obj_arr
order
end
def order_json_representation(order_obj)
template_name = "order.rabl"
template_path = Rails.root.join("app", "json_views")
RablRails.render(order_obj, template_name, view_path: template_path, format: :json)
end
end
Hope this helps.
Thanks.
I'm trying to write my test to ensure creating a new book with genres assigned to it works.
I am using Active Model Serializer with the JSON_API structure (http://jsonapi.org/)
Book Model File
class Book < ApplicationRecord
belongs_to :author, class_name: "User"
has_and_belongs_to_many :genres
end
Genre Model File
class Genre < ApplicationRecord
has_and_belongs_to_many :books
end
Book Serializer file
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :adult_content, :published
belongs_to :author
has_many :genres
end
Test Sample Data
def setup
...
#fantasy = genres(:fantasy)
#newbook = {
title: "Three Little Pigs",
adult_content: false,
author_id: #jim.id,
published: false,
genres: [{title: 'Fantasy'}]
}
end
Test Method
test "book create - should create a new book" do
post books_path, params: #newbook, headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
puts "json = #{json}"
assert_equal "Three Little Pigs", json['data']['attributes']['title']
genre_data = json['data']['relationships']['genres']['data']
puts "genre_data = #{genre_data.count}"
assert_equal "Fantasy", genre_data
end
Book Strong Params
def book_params
params.permit(:title, :adult_content, :published, :author_id, :genres)
end
Test Result (console response)
# Running:
......................................................json = {"data"=>{"id"=>"1018350796", "type"=>"books", "attributes"=>{"title"=>"Three Little Pigs", "adult-content"=>false, "published"=>false}, "relationships"=>{"author"=>{"data"=>{"id"=>"1027431151", "type"=>"users"}}, "genres"=>{"data"=>[]}}}}
genre_data = 0
F
Failure:
BooksControllerTest#test_book_create_-_should_create_a_new_book [/Users/warlock/App_Projects/Raven Quill/Source Code/Rails/raven-quill-api/test/controllers/books_controller_test.rb:60]:
Expected: "Fantasy"
Actual: []
bin/rails test test/controllers/books_controller_test.rb:51
Finished in 1.071044s, 51.3518 runs/s, 65.3568 assertions/s.
55 runs, 70 assertions, 1 failures, 0 errors, 0 skips
As you can see from my JSON console log, it appears my genres are not being set(need to scroll to the right in the test output above).
Please ignore this line:
assert_equal "Fantasy", genre_data
I know that's wrong. At the moment, the json is showing genre => {data: []} (empty array), that's the thing I'm trying to solve at the moment.
How do I go about creating a book with genres in this case, any ideas? :D
This is just sad...third time this week, I am answering my own question.
I finally found the answer from this Stackoverflow question:
HABTM association with Strong Parameters is not saving user in Rails 4
Turns out my strong parameters need to be:
def book_params
params.permit(:title, :adult_content, :published, :author_id, {:genre_ids => []})
end
Then my test data can be:
#fantasy = genres(:fantasy)
#newbook = {
title: "Three Little Pigs",
adult_content: false,
author_id: #jim.id,
published: false,
genre_ids: [#fantasy.id]
}
Update my test method to:
test "book create - should create a new book" do
post books_path, params: #newbook, headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
assert_equal "Three Little Pigs", json['data']['attributes']['title']
genre = json['data']['relationships']['genres']['data'].first['title']
assert_equal "Fantasy", genre
end
Now my test passes.
An AssessmentItem has many ItemLevels and one ItemLevel belongs to an AssessmentItem.
In my model I have
has_many :item_levels
accepts_nested_attributes_for :item_levels
When updating an Item, you should be able to specify what levels should be associated with that Item. The update action should receive the parameters specified for levels and create new ItemLevel objects that are associated with the Item being updated, and delete any levels that we previously associated and not specified when updating. However, when I try to create new levels, I get an ActiveModel::ForbiddenAttributes error.
Controller:
def update
#item = AssessmentItem.find(params[:id])
old_levels = #item.item_levels #active record collection
#item.update_attributes(update_params)
convert_levels = old_levels.map {|l| l.attributes} #puts into array
(items - convert_levels).each{|l| ItemLevel.create(l)} ##causes error
(convert_levels - level_params).each { |l| ItemLevel.find(l["id"]).destroy }
end
end
private
def level_params
params.require(:assessment_item).permit(:item_levels => [:descriptor, :level])
end
def update_params
params.require(:assessment_item).permit(:slug, :description, :name)
end
This is my json request in Postman:
{
"assessment_item": {
"slug" : "newSlug",
"description" : "NewDescriptiong",
"name" : "different name",
"item_level_attributes":[
{
"descriptor":"this should be new",
"level":"excellent"
}
]}
}
How can I get my action to allow the parameters? How can I effectively pass them to the factory? Thanks.
I think you should also permit item_level_attributes in update_params like this:
def update_params
params.require(:assessment_item).permit(:slug, :description, :name, :item_levels => [:descriptor, :level])
end
or
def update_params
params.require(:assessment_item).permit(:slug, :description, :name, :item_level_attributes => [:descriptor, :level])
end
When I create auto documented API specification, I faced with problem of passing complex object (ActiveRecord for ex.) to param function of swagger-docs/swagger-ui_rails, because it takes only simple types (string, integer, ...).
I solved this trouble with next metaprogramming ruby trick:
class Swagger::Docs::SwaggerDSL
def param_object(klass, params={})
klass_ancestors = eval(klass).ancestors.map(&:to_s)
if klass_ancestors.include?('ActiveRecord::Base')
param_active_record(klass, params)
end
end
def param_active_record(klass, params={})
remove_attributes = [:id, :created_at, :updated_at]
remove_attributes += params[:remove] if params[:remove]
test = eval(klass).new
test.valid?
eval(klass).columns.each do |column|
unless remove_attributes.include?(column.name.to_sym)
param column.name.to_sym,
column.name.to_sym,
column.type.to_sym,
(test.errors.messages[column.name.to_sym] ? :required : :optional),
column.name.split('_').map(&:capitalize).join(' ')
end
end
end
end
Now I can use param_object for complex objects as param for simple types :
swagger_api :create do
param :id, :id, :integer, :required, "Id"
param_object('Category')
end
Git fork here:
https://github.com/abratashov/swagger-docs
I have the following dynamic params depending on the line items i am trying to add to an order
{"line_item" => {"items"=>{"0"=>{"price"=>"5.75", "name"=>"Item name", "quantity"=>"5"}, "1"=>{"price"=>"3.35", "name"=>"Item name", "quantity"=>"1"}}}
In my controller:
def lineitems_params
params.require(:line_item).permit(:key1, :key2, :key3, :key4, :payment_type, :payment_provider).tap do |whitelisted|
whitelisted[:items] = params[:line_item][:items]
end
end
I still get the
Unpermitted parameters: items
in my logs, and it does not update the items.
How can i solve this?
NOTE: the items hash can have many elements inside.
EDIT:
In my model:
serialize :items, Hash
This should work
def lineitems_params
params.require(:line_item).permit(:key1, :key2, :key3, :key4, :payment_type, :payment_provider, {:items => {:price, :name, :quantity}})
end
Update
may be you should just give like this
def lineitems_params
params.require(:line_item).tap do |whitelisted|
whitelisted[:items] = params[:line_item][:items]
end
end
Source
Note: Don't give params.require(:line_items).permit! it permits all attributes.