Testing an AJAX POST using Rack::Test - how to pass in data? - ajax

I'm using Rack::Test to test my app and need to test the posting of data via AJAX.
My test looks like:
describe 'POST /user/' do
include Rack::Test::Methods
it 'must allow user registration with valid information' do
post '/user', {
username: 'test_reg',
password: 'test_pass',
email: 'test#testreg.co'
}.to_json, {"CONTENT_TYPE" => 'application/json', "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
last_response.must_be :ok?
last_response.body.must_match 'test_reg has been saved'
end
end
But at the server end it's not receiving the POSTed data.
I also tried just passing in the params hash without the to_json but that made no difference.
Any idea how to do this?

Your post endpoint must parse the posted JSON body itself, which I assume you already do. Can you post how your end point works, also the rack-test, rack,ruby and sinatra version numbers? Please mention also how you test whether the server's receiving anything -- namely test mockup may confuse your detection.
post '/user' do
json_data = JSON.parse(request.body.read.to_s)
# or # json_data = JSON.parse(request.env["rack.input"].read)
...
end

Okay so my solution is a little weird and specific to the way I am triggering my JSON request in the first place, namely using jQuery Validation and jQuery Forms plugins on the client end. jQuery Forms doesn't bundle the form fields into a stringified Hash as I'd expected, but sends the form fields via AJAX but as a classic URI encoded params string. So by changing my test to the following, it now works fine.
describe 'POST /user/' do
include Rack::Test::Methods
it 'must allow user registration with valid information' do
fields = {
username: 'test_reg',
password: 'test_pass',
email: 'test#testreg.co'
}
post '/user', fields, {"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
last_response.must_be :ok?
last_response.body.must_match 'test_reg has been saved'
end
end
Of course this is specific to the way the jQuery Forms plugin works and not at all how one would normally go about testing the POSTing of JSON data via AJAX. I hope this helps others.

Related

Cant set custom header params with rspec and Grape

I use Grape api and i need to write a test with custom header
my code:
it "should accept message" do
post "/api/v1/my/route", post_data, secret: "ASDFGHJKL"
last_response.status.should == 201
end
but the route gets no headers at all,
i also tried headers['secret'] = "ASDFGHJKL"
and also request.env['secret']
nothing works.
how can i pass headers in rspec to grape route?
Did you try header 'secret', 'ASDFGHJKL'?
More on the rack-test docs
Try using #request variable, e.g. #request.env['secret'] = 'ASDF..'. Hope this helps.
Also, take a look at this - Sending custom headers through RSpec

Controller can not detect ajax requests

I am using simple_form gem and generating the form I am specifying the remote:true option like this:
<%= simple_form_for #webinar, validate: true, remote:true do |f| %>
So, the output html for the form is the following fragment:
<form accept-charset="UTF-8" action="/webinars" class="simple_form new_webinar" data-remote="true" data-validate="true" enctype="multipart/form-data" id="new_webinar" method="post" novalidate="novalidate"> ... </form>
As I checked, using the standard form_for helper is adding the data-remote='true' to the form when remote:true options is used. And as you can see from the generated html, when I am using the simple_form gem there is such attribute, too.
So, in my controller I have:
def create
#webinar = Webinar.new(params[:webinar])
respond_to do |format|
if #webinar.save
format.html { redirect_to #webinar, notice: 'Webinar was successfully created.' }
format.js
format.json { render json: #webinar, status: :created, location: #webinar }
else
format.html { render action: "new" }
format.json { render json: #webinar.errors, status: :unprocessable_entity }
end
end
end
But, always the format.html is used. What i am doing wrong?
EDIT:
I have used logger.debug request.format to check what is the actual format ask for and in the log file it was:
text/html
So, the issue must be in the simple_form generated form - what can be wrong there when we have "data-remote=true"?
You're confusing format.json|format.html with remote: true. Both are different. The presence of remote: true does not imply format.json.
format.json does not indicate that the URL was invoked via javascript. It only means that the client expects JSON output. i.e. it does not indicate where input came from, it indicates what output is required.
The general use of remote:true is, instead of reloading the page, you submit the form as an Ajax request and then show the response in a JQuery popup or something. But if you want to display the response as a JQuery popup - you need HTML output, not JSON output right?
Some people use remote: true to load HTML content in a popup. Your use case is to do remote: true but you're expecting JSON formatted data. Rails cannot make these decisions for you. It by default sends the request to /webinars and expects that you will handle the HTML response. If you really want JSON response - then customize the URL to which the Ajax request is posted:
simple_form_for #webinar, url: webinar_path(format: :json), .....
If you do the above, now the webinar controller will be called with JSON format.
Overall:
remote:true can be used with both format.html and format.json
The majority use case in a Rails application is to handle remote: true as a controller request as usual, render a partial HTML template (i.e. the response content alone without the overall page layout/navigation/etc) and send it back as HTML to be displayed in a popup
Most people just blanket-handle remote callbacks and display a JQuery popup. So they don't need to write individual code for each remote forms
So by default, Rails calls format.html for remote requests
If you specifically want format.json, and if you really want to handle the JSON manually on the client, change the URL accordingly. But this is not the majority use case
Making an Ajax request does not mean the content-type is JSON. It is a HTML request made using Javascript. e.g. In jquery $.ajax method, check these two options: accept and dataType. If you really want to send Accepts: application/json header, then you have to manually specify that when making the Ajax request (or you need to end the url with .json if its a Rails app).
So even if you make a normal Ajax request to /webinars, like $.ajax('/webinars', ...) - it won't go to format.json ! It will still only go to format.html. If you really want a JSON format, then you must say $.ajax('/webinars', { accepts: 'application/json' }), or you must say $.ajax('/webinars.json')
Edit: Minor clarification
It seems like you are not directly requesting a JSON formatted document in the generated form action. Perhaps one option is to set :format to json in your routes file using this technique: http://guides.rubyonrails.org/routing.html#defining-defaults
You can also define other defaults in a route by supplying a hash for the :defaults option. This even applies to parameters that you do not specify as dynamic segments. For example:
match 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
Rails would match photos/12 to the show action of PhotosController, and set params[:format] to "jpg".
I can not believe that I have lost so much time in trying to understand why the simple_form is not working as expected.
Finally, it appears that I have done everything in the right way and the issue was caused because:
AJAX can not be used for file uploads.
In order to solve my problem I simply have to add the following gem into my Gemfile and run the bundle install command:
gem 'remotipart', '~> 1.0'
Then add the following line in my application.js file:
//= require jquery.remotipart
More information about:
remotipart gem
How to upload multiple files using jQuery

Why am I getting a 405 on a POST request for JSON?

So I've got a Padrino app with a controller that looks roughly like this (simplified for brevity):
App.controllers :questions do
get :index, :with => :id, :provides => [:html, :json] do
# Stuff
end
post :index, :with => :id, :provides => [:html, :json] do
# Other stuff
end
end
If I hit "questions/1" in my browser, I see the HTML page for the given question. If I hit "questions/1.json", I see the JSON representation of the question. Exactly like I'd expect.
Now, if I submit a POST request to "questions/1" via a web form, it works. But if I try to send a POST request to "questions/1.json" (signaling that I want the response in JSON format—or at least that's how I thought it worked), I get a 405 Method Not Allowed.
I'm guessing there's something basic I'm misunderstanding here. What am I missing? More importantly, how should I define a route to accept POST requests and provide either HTML or JSON responses?
Well, I'm not really sure why this was happening; but for now I've gotten around the issue by setting the "ACCEPT" header in my POST request to "application/json" instead of tacking ".json" onto the end of the URL (and upon my limited internet research, this may be the preferred approach anyway).

Passing Sinatra models and validation

As mentioned on another similar thread I started recently, I am porting an ASP MVC application to Sinatra, partly for learning, partly for production purposes.
Currently my ASP MVC application has no views, it just exposes models as Xml/Json etc and accepts them in the same way. So in the scenario of someone sending a model to the server, it would auto bind the object, perform validation then return validation errors if there are any, if not carry on with the relevant actions.
Now in Sinatra it is a bit more barebones that ASP MVC which is a great thing as I have more choice on how to do these parts, however I dont have a clue what gems/libraries functionality is available for doing this.
My ideal scenario would be I have my pure html/js front end posting a model as json and then have it turned into an actual model where I can validate it (through Sinatra or other framework) then make a decision based on the validation results.
Try taking a look at this gem:
json
Then what you should be able to do is pass in the attributes for an object you would like to create as a hash of attributes converted to a json object:
{:attribute1 => "value1", :attribute2 => "value2"}.to_json
And on your Sinatra app, catch them and build the object from those attributes, and you can use Sinatra to validate as well:
def '/create_object' do
content_type :json
obj = Object.new(JSON.parse(params[:object]))
if obj.save
obj.to_json
else
error 400, user.errors.to_json
end
end
Sounds like an ordinary case for an ORM. So you could for example use DataMapper. With that in your hands, you can simply send a form via HTTP to a specific route and do something like:
post '/create' do
#post = Post.new(params[:your_form])
if #post.save
erb :a_template
else
erb :your_form
end
end
Where params[:your_form] is a hash containing the contents of your form. No JSON needed at all. If the saving process fails (for validation reason or whatever), the model object #post will have errors, which you could process in a different view (:your_form for example).
Best Regards
Tobias

Manual POST request

Scenario: I have logged into a website, gained cookies etc, got to a particular webpage with a form + hidden fields. I now want to be able to create my own http post with my own hidden form data instead of what is on the webpage and verify the response instead of using the one on the webpage.
Reason: Testing against pre-existing data (I know, I know) which could be different on each environment hence no predictable way to use it. We need a workaround.
Is there any way to do this without manually editing the existing form and submitting that? Feels a little 'hacky'.
Ideally, I would like to say something like:
browser.post 'url', 'field1=test&field2=abc'
I would probably switch to mechanize to muck around at the protocol level. Something like this added to your script
b = WWW::Mechanize.new
b.get('http://yoursite.com/current_page') do |page|
# Submit the login form
my_form = page.form_with(:action => '/post/url') do |f|
f.form_loginname = 'tim'
f.form_pw = 'password'
end.click_button
end

Resources