RSpec / FactoryGirl - How to pass params in post request spec - ruby-on-rails

I have a Rails API backend set up with restful routes. I'm trying to pass in parameters for a post request spec, but I've run into some troubles.
In my app a user has many timelines, and a timeline has many entries.
I can correctly create a timeline with a post request by doing the following:
post '/timelines', '{ "timeline": { "title":"My timeline", "body":"My timeline body"}}', #token_header
The problem i have is when i have to pass more than just string, i.e. an image. My entry model has an image attribute, but I can't figure out how to pass in into the parameters.
I have the following test which illustrates what i want to do. As you can see I've declared some entry_attributes but I can't figure out how to correctly pass them as I did with the timeline params.:
context 'POST /timelines/:id/entries' do
it 'creates an entry for the given timeline' do
entry_attr = FactoryGirl.attributes_for(:entry)
post "/timelines/#{#timeline.id}/entries", {}, #token_header
expect(response.status).to eq 201
end
end
Any help is greatly appreciated...

Related

How to create requests params for json api resources in requests tests?

I'm creating a API with Rails 6 and JSON API Resource in order to learn more. I got stuck with requests tests. What I'm doing for now is using the gem json_matchers to test the response of my endpoint, but my real problem is to build the body of requests for: post, put and patch.
Hi, thanks for you attention.
Here is the deal, I started adding to my RSpec files all the json needed for the tests, but these files ended up getting too big. So I thought, how can I makes this to stay more lean?
Then I copy all the json in my tests to json files and importing when needed, for example:
# RSpec Helper
def request_json(json_name)
request_directory = "#{Dir.pwd}/spec/support/api/requests"
request_path = "#{request_directory}/#{json_name}.json"
File.read(request_path)
end
# Example of params for user request
let(:params) { request_json("user") }
I thoutgh it was a great idea, but then I ran into two problems: I need to create a file for every request test and... how I will modify same value of this json file at runtime? For example, I create a object using FactoryBot and now I need to use the ID of this object in my request for a update. How can I do that?
Using regex?
let(:params) do
json = request_json("user")
json.gsub(/\"id\": \"1\"/, "\"id\": \"#{id}\"")
end
Ok... It works, but I don't like it! I think that this can turn to be a real mess. Another options was to convert this to hash and then json again... Nope, nope, nope, don't like it either.
Now I'm trying to create something more dynamic using FactoryBot and Faker. For now, with the code below, I can pass a factory name and receive a perfect json body for a post.
def serializer_for(resource)
{
"data": {
"type": resource.to_s.tableize,
"attributes": create_attributes_for(resource),
}
}.to_json
end
def create_attributes_for(resource)
attributes = attributes_for(resource)
attributes.reduce({}) do |hash, element|
hash.update(standardize_json_key() => element.last.to_s)
end
end
def standardize_json_key(symbol)
symbol.to_s.gsub("_", "-")
end
But then I began to think about the challenges of this approach:
Post/Put/Patch with relationships?(has_one or has_many)
How I'll add the ID field for put/path?
Add how I'll add the upload files when needed?
So... returning to my question: How can I prepare the json body for the request using factory bot (using even traits) with all the concerns above?
If you have/know a better answer for this problem, share with, please. If not, I'll try to create a gem for this.
Appreciate your time, thanks!

How do I post json data in an rspec feature test powered by Capybara

Short question
I need to post JSON in feature tests, something like in my controller tests:
post '/orders.json', params.to_json, format: :json
But I need to do it in feature tests, and page.driver.post only posts form data. How do I post JSON in Capybara?
Long Explanation
I have a (non-Rails) app (let's call it the planet) that posts JSON to my rails app (call it a star) to create a record, and then forwards the user to the url for the show action.
I'm creating a feature spec, but since the first interaction isn't part of the Rails, app, I need to mock it using JSON.
I've been using this:
page.driver.post '/orders.json', params.to_json
But this seems to post as a form, whereas my planet application posts JSON. This leads to some really funky parameter problems, where parsing JSON gives me different params from form-data.
How do I post JSON in Capybara?
TL;DR - you don't
Capybara is designed to emulate a user for feature tests. Hence why the post method is driver specific (page.driver.xxx) and really isn't intended for use directly by tests. A user can't just submit a POST without a page to submit it from. Therefore, if you do actually need to test this via feature tests, the best solution is to create a small test app that provides a page you can have Capybara visit which will automatically (or on button click, etc) have the browser make the AJAX post to the app under test and handle the response.
So, turns out that it just isn't possible with Capybara. See Thomas Walpole's answer for more details.
As a workaround, I used httparty:
require 'httparty'
RSpec.feature 'Checkouts', type: :feature do
include HTTParty
base_uri 'http://localhost:3000'
private
def checkout_with(cart)
post orders_path(format: :json).to_s,
body: cart.to_json,
headers: { 'Content-Type' => 'application/json' }
end
end

Ruby on rails api post request issue

i am trying to post using postman to a rails api that i made, the actual request goes in and creates an entry, but nothing but the ID gets recorded. attached are the files for that.
You need to pass the post params and not just the id into the list.new call and make sure you're sending up the correctly namespaced values in the post request.
Step 1.
In create you need to do
#list = List.new(list_params)
Step 2.
Postman needs to be putting all the params into the list[] namespace
ie. list[title] rather than just title.

Rails functional test: sending URL query parameters in POST request

I'm sending a POST request in a Rails functional test like this:
post :create, collection: { name: 'New Collection' }
collection gets sent as JSON-encoded form data, as expected.
What I can't figure out is how to add a query to the URL. The documentation says that I can access the request object and modify it before it gets sent. So I tried this:
#request.GET[:api_key] = 'my key'
post :create, collection: { name: 'New Collection' }
But, :api_key never appears in the request.GET hash on the server. (It does when I send it though another HTTP client, though.)
A little background first to clarify things: although a request cannot be both GET and POST at the same time, there is nothing stopping you from using both the query string and body form data when using POST. You can even have a POST with all parameters in the query string and an empty body, though this sounds quite unusual.
Rails supports this scenario and indeed you can easily send a form using a POST request and still have query in the form's action. The query will be accessible with request.GET hash (which is an alias of query_string), while the POST body params with the request.POST hash (an alias of request_parameters). The params hash is actually constructed from the combined GET and POST hashes.
However, from my research it seems that Rails does not support passing query string in POST requests in functional controller tests. Although I could not find anything regarding this in any documentation or among known issues on github, the source code is quite clear. In the following text, I'm assuming that you use Rails 4.
Why it does not work
The problem with functional controller tests is that they don't use real requests / responses but they simulate the HTTP handshake: the request is mocked up, its parameters filled in appropriate places and the given controller action is simply called as a normal ruby method. All of this is done in the action_controller/test_case classes.
As it turns out, this simulation is not working in your particular case, due to two reasons:
The parameters passed in when running the test are always handed over either to the request_parameters, i.e. the request.POST hash when using a post request or to the query_string (i.e. request.GET) for get test requests. There is no way for both of these hashes to be set during a single test run.
This actually makes some sense as the get, post, etc. helpers in functional tests accept only a single hash of params so the internal test code cannot know how to separate them into the two hashes.
It is true that one can set up the request before running the test using the #request variable, but only to a certain extent, you can set headers, for example. But you cannot set internal attributes of the request, because they are recycled during the test run. The recycling is done here and it resets all internal variables of the request object and the underlying rack request object. So if you try to set up the request GET parameters like this #request.GET[:api_key] = 'my key', it won't have any effect as the internal variables representing this hash will get wiped during recycling.
Solutions / workarounds
Give up functional testing and choose integration tests instead. Integration tests allow to set the rack environment variables separately from the main parameters. The following integration test passes the QUERY_STRING rack env variable besides the normal post body params and should work flawlessly:
class CollectionsTest < ActionDispatch::IntegrationTest
test 'foo' do
post collections_path, { collection: { name: 'New Collection' } },
{ "QUERY_STRING" => "api_key=my_api_key" }
# this proves that the parameters are recognized separately in the controller
# (you can test this in you controller as well as here in the test):
puts request.POST.inspect
# => {"collection"=>{"name"=>"New Collection"}}
puts request.GET.inspect
# => {"api_key"=>"my_api_key"}
end
end
You can still use most of the features from functional tests in your integration tests. E.g. you can test for assigned instance variables in the controller with the assigns hash.
The transition argument is supported also by the fact that Rails 5 will deprecate functional controller tests in favor of integration testing and since Rails 5.1 these functional tests support will be moved out to a separate gem.
Try Rails 5: although functional tests will be deprecated, its source code seems to have been heavily rewritten in the rails master and e.g. recycling of the request is not used any more. So you might give it a try and try to set the internal variables of the request during test setup. I have not tested it though.
Of course, you can always try to monkey-patch the functional test so that it supports separate params for the query_string and request_parameters hashes to be defined in tests.
I'd go the integration tests route :).
I assume that the controller is named CollectionsController, and its route to create action is /collections (if not, you just have to adapt the example bellow)
And I also assume you are in a request spec
This should work:
post '/collections?api_key=my_key', collection: { name: 'New Collection' }
The 2nd argument to post is a hash of all the params you'll receive in the controller. Just do this:
post :create, collection: { name: 'New Collection' }, more_params: 'stuff', and_so_on: 'things'
Those params will be available in the controller:
params[:and_so_on] == 'things'
You want to send a POST request:
I'm sending a POST request in a Rails functional test like this:
But you want to retrieve data from a GET request:
But, :api_key never appears in the request.GET hash on the server.
A request cannot be GET and POST at the same time, if you are sending a POST request and pass parameters in the query string then you would have those parameter values available on a POST request, GET just won't have anything.
Then:
#request.GET[:api_key] = 'my key'
post :create, collection: { name: 'New Collection' }
You are modifying the GET values on the request, but then you actually send a POST request which means that when the post method gets called and the request is sent to the server only what you sent on the POST will be available. Just send the api key bundled with the POST request (could be inside the collection hash for that matter)
This is also a problem when testing POST actions with RSpec (v3.4).
A workaround is to mock the return value of request.GET or request.query_string methods.
it "should recognise a query parameter in post action" do
allow(subject.request).to receive(:query_string).and_return("api_key=my%20key")
#params = {collection: { name: 'New Collection' }}
expect(subject.request.query_string).to eq "api_key=my%20key"
post :create, #params
end

RSpec approach to test XML and HTTP responses?

I have a RESTful site that uses both the XML and web responses (API and web site). Since there are a lot of pages, my current goal is setting up RSpec to simply request each of the pages in both data formats and check if the returned response is 200. What is the best way to check for both XML and HTTP 200 response? I know I should be doing TDD upfront, but right now I need this as a shell.
Example: I want to request both "/users" and "/users.xml" and test if there weren't any server errors (200 OK)
I wrote a blog post on testing JSON APIs with RSpec a couple of weeks ago.
Basically, the way we are doing it is to get the actual response, and parse it to make sure it has the right content. As an example:
context "#index (GET /artworks.json)" do
# create 30 Artwork documents using FactoryGirl, and do a HTTP GET request on "/artworks.json"
before(:each) do
30.times { FactoryGirl.create(:artwork) }
get "/artworks.json"
end
describe "should list all artworks" do
# the request returns a variable called "response", which we can then make sure comes back as expected
it { response.should be_ok }
it { JSON.parse(response.body)["results"].should be_a_kind_of(Array) }
it { JSON.parse(response.body)["results"].length.should eq 30 }
# etc...
end
end
Obviously a simple example, but hopefully you get the idea. I hope this helps.

Resources