How to upload a file in a Rails Rspec request spec - ruby-on-rails

I want to test uploading a file in a Rails Rspec request test. I'm using Paperclip for asset storage.
I've tried:
path = 'path/to/fixture_file'
params = { file: Rack::Test::UploadedFile.new(path, 'application/pdf', true) }
post v1_product_documents_path, params: params
In the controller, I get a string
"#Rack::Test::UploadedFile:0x0055b544479128>"
instead of the actual file.
The same code works in controller tests

try to use fixture_file_upload:
fixture_file_upload
or
if you wanted to use this in a factory
Rack::Test::UploadedFile.new(File.open(File.join(Rails.root, '/spec/fixtures/images/bob-weir.jpg')))

In rails_helper.rb do
include ActionDispatch::TestProcess
include Rack::Test::Methods
Then you have few options.
1. You can use fixture_file_upload helper
let(:file) { fixture_file_upload('files/image.jpg') }
it 'it should work' do
post some_path, params: { uploads: { file: file } }
end
You can use Uploaded file but you have to give full path
let(:file) { Rack::Test::UploadedFile.new(Rails.root.join('spec',
'fixtures', 'blank.jpg'), 'image/jpg') }
let(:params) { { attachment: file, variant_ids: ['', product.master.id] } }
let(:action) { post :create, product_id: product.slug, image: params }

For rails 6 no need to do includes, just fixture_file_upload. If you are using spec/fixtures/files folder for fixtures you can use file_fixture helper
let(:csv_file) { fixture_file_upload(file_fixture('file_example.csv')) }
subject(:http_request) { post upload_file_path, params: { file: csv_file } }

Under describe block, include these modules
include Rack::Test::Methods
include ActionDispatch::TestProcess
Now try running the specs
path = 'path/to/fixture_file'
params = { "file" => Rack::Test::UploadedFile.new(path, 'application/pdf', true) }
post v1_product_documents_path, params: params
Also, I guess you forgot to add , between v1_product_documents_path and params: params, please add that and let me know.
Hope that helps!

Might be useful for other users: I got this problem because I mistakenly used a get instead of a post request in my specs.

For others still getting something like "#Rack::Test::UploadedFile:0x0055b544479128>" in the controller after implementing the approved solution above, I resolved this by changing my content-type header to be 'application/x-www-form-urlencoded' so that form data would be accepted.

file = File.expand_path("path/to/fixture_file", __dir__)
Rack::Test::UploadedFile.new(file, 'application/pdf')

If you need the content type in your RSpec test, it seems you can pass it as an argument to fixture_file_upload (no idea why this method does not derive the content type from the OS).
For instance:
fixture_file_upload("pdfs/some.pdf", "application/pdf")

I ran into this issue while writing request specs for a POST endpoint that processes incoming email payloads that include file attachments.
this is what the request looked like in the request spec:
post api_v1_email_processor_path, params: params, as: :json
where params[:attachment] was:
fixture_file_upload("spec/support/fixtures/files/report.csv", 'text/csv',)
and when I added some breakpoints in the controller, this is what the attachment was being converted into:
{"original_filename"=>"report.csv", "tempfile"=>"#<File:0x0000000121d2bfc8>", "content_type"=>"text/csv"}
as you can see, the "tempfile" was being cast to a string value of "<File:0x0000000121d2bfc8>"
when I emulated the same request in a controller spec, to my huge confusion, there was no casting, and the breakpoint showed the param (correctly) as:
#<ActionDispatch::Http::UploadedFile:0x000000012d559488 #tempfile=#<Tempfile:/var/folders/jg/n_0rknb97kvddb0jgh6r3_x40000gn/T/RackMultipart20221008-45122-mzyqg5.csv>, #original_filename="report.csv", #content_type="text/csv", #headers="Content-Disposition: form-data; name=\"attachment1\"; filename=\"report.csv\"\r\nContent-Type: text/csv\r\nContent-Length: 24931\r\n">]
after seeing L.Youl's answer https://stackoverflow.com/a/67987482/14926068, it got me thinking that the issue was with how the request payload was being formatted, prior to being sent to the server through the Rack middleware. and so I tried taking off the as: :json:
post api_v1_email_processor_path, params: params
and bingo, I got the same result as with the controller spec.
after inspecting response.request.content_type in my request spec, I found that when I took the as: :json off, its value changed from "application/json" to
"multipart/form-data; boundary=----------XnJLe9ZIbbGUYtzPQJ16u1"
for completeness, I checked response.request.content_type in the controller spec as well, and found that even with as: :json in the request, the content_type was being set to "multipart/form-data", and that accounted for the discrepancy in behavior between controller and request specs - it seems that as: :json only affects the "content-type" in the request specs.
in retrospect, it was obvious - json cannot be used to send actual files. but it took me a long time to figure it out because the equivalent controller spec was working fine and this was literally the only endpoint in my Rails api-only app that expected non-json payloads

Related

No route matches [GET] on a post request

I started Ruby on Rails few days ago, and I'm struggling with routing.
Indeed, I would like to make a post request through my routes.rb, but I keep having a
No route matches [GET] "/orders/refresh"
error.
Here is my routes.rb :
# frozen_string_literal: true
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get '/orders', to: 'orders#index'
get '/orders/active(/:q)', to: 'orders#active'
post '/orders/refresh', to: 'orders#refresh'
end
and here is my controller (orders_controller.rb) :
# frozen_string_literal: true
class OrdersController < ApplicationController
def index
#orders = Order.order(:departure_date).all
render json: #orders.to_json
end
def active
if !params[:q]
#orders = Order.order(:departure_date).where(active: true)
else
#orders = Order.order(:departure_date).where("reference = ? OR client_name = ? OR departure_city = ? OR arrival_city = ?",
params[:q], params[:q], params[:q], params[:q])
.where(active: true)
end
render json: #orders.to_json
end
def refresh
response = RestClient.get 'https://wakeo-technical-test.s3.eu-west-3.amazonaws.com/api.json'
json = JSON.parse response
if !json.nil?
json.each do |order|
old_order = Order.find_by(reference: order["client_number"])
if !old_order.nil?
old_order.update(departure_date: order["dep_time"])
old_order.update(arrival_date: order["arr_time"])
old_order.update(client_name: order["company"])
old_order.update(departure_city: order["dep_city"])
old_order.update(arrival_city: order["arr_city"])
end
end
else
puts "error seeding external API"
end
end
end
From what I have understood, it seems like RoR will try to find a GET request for that specific URL, and since it won't find any, it will throw that error. How could I make that request be a POST for Rails ?
Also, I would appreciate any suggestion about how I should use ActiveRecord Querying, I'm pretty sure I could do it better here.
Thanks, have a great day !
EDIT : Here is the list of different routes my app seems to be capable of, including my POST.
Routes and error
The most common reason you unexpectly get GET requests instead of PUT, PATCH, POST or DELETE is that you are using link_to 'Something', '/some_path', method: :post and you broke the Rails Unobtrusive Javascript Driver (Rails UJS):
Because submitting forms with HTTP methods other than GET and POST
isn't widely supported across browsers, all other HTTP methods are
actually sent over POST with the intended method indicated in the
_method parameter. Rails automatically detects and compensates for this.
Rails does that with a JavaScript event handler attached to any link with the data-method attribute. But if you broke that functionality the browser will just perform its default action which is sending a GET request when the user clicks a link.
This problem usually boils down to one or more of:
Your javascript is throwing an error which halts script execution (use the browser console to find the error, make it suck less).
Rails UJS is not included in your assets pipeline or webpacker packs and thus not in the page.
The quick and easy solution to sidestep the problem is by using button_to which actually creates a form and does not require any JavaScript trickery. After all forms can send POST requests. And by just passing a _METHOD hidden field Rack will treat the request as any other HTTP verb.
button_to 'Something', '/some_path', method: :post
But in the long run you should probably fix the problem if you want to use any of the features of Rails UJS.
Your routes.rb is expecting a POST request to /orders/refresh routes, but apparently you are testing with a GET request.
Try changing your routes.rb:
Rails.application.routes.draw do
# ...
get '/orders/refresh', to: 'orders#refresh'
end
... or change your request to a POST request. If you are using Rails forms, you must do something like this:
form_with(url: "/orders/refresh", method: "post")
Ok, I think I figured it out.
It might be because when I hit /orders/refresh directly in my web browser, it will try to find a GET corresponding to the request.
I managed to make POST using a client like Postman, and everything works fine.
Thank you for your help !

Problem with Migrating Rspec Controller Tests from Rails 4.2.10 to Rails 5.1.4

Old Rails 4.2.10 line:
post :show, "Some XML as String"
to Rails 5.1.4
post :show, params: { ??? }
So what is the key value pair I add here?
Edit:
So currently in the tests its written like this:
let(:logout_request_xml) { "<soap-env:Envelope xmlns:soap-env='http://schemas.xmlsoap.org/soap/envelope/'><soap-env:Body>...more stuff...</soap-env:Body></soap-env:Envelope>" }
...
post :show, logout_request_xml
I've been able to POST raw data using Rails 5.1.4 by setting it directly in the request for a controller spec
#request.env['RAW_POST_DATA'] = '<test>some raw xml</test>'
post :show
In the controller this can then be read via the request.body.read
> request.body.read
=> "<test>some raw xml</test>"
Note that this will not work when moving to Rails 5.2. In that case the request body will be empty as the underlying behavior has changed. The best way I've found to test this scenario is to use Request Specs instead of Controller Specs.
Controller specs - A controller spec is an RSpec wrapper for a Rails functional test. It allows you to simulate a single http request in each example, and then specify expected outcomes
Request specs - Request specs provide a thin wrapper around Rails' integration tests, and are designed to drive behavior through the full stack, including routing (provided by Rails) and without stubbing (that's up to you).
Here is an example posting that same data via a Request Spec:
post items_path, env: {'RAW_POST_DATA' => "<test>some raw xml</test>"}
In the controller
> request.body.read
=> "<test>some raw xml</test>"
None of the above answers worked for me in Rails 5.2.2.
I was using code like this #request.env['RAW_POST_DATA'] = json in 5.1 in some cases where I needed a special type of serialization and I converted all of those to:
json = { "some" => "JSON" }.to_json
post :show, body: json
Here is how you can pass raw body in the minitest:
post :show, as: :xml, headers: { 'RAW_POST_DATA': 'Some XML as String' }
You can't use params because it's not suppose to be used in this way, also you need to add as: :xml becuase you send raw body with application/x-www-form-urlencoded content-type.
In the controller request.raw_body

Rails 5 API integration tests don't work with jsonapi-resources

I can't test POST requests against my jsonapi-resources Rails 5.1 API. Rails does not seem to allow me to customize request content types, or is doing it wrong.
jsonapi-resources version 0.9.0, edge Rails (I think it's 5.2 beta2)
So, this IntegrationTest code:
require 'test_helper'
class EventsControllerTest < ActionDispatch::IntegrationTest
setup do
#event = events(:one)
end
test 'should get index' do
get events_url, as: 'application/vnd.api+json'
assert_response :success
end
test 'should create event' do
assert_difference('Event.count') do
post events_url, params: {
data: {
type: 'events',
attributes: {
name: #event.name,
body: #event.body
}
}
},
as: :api_json
end
assert_response 201
end
end
...produces this error:
$ bin/rails test
...
Failure:
EventsControllerTest#test_should_create_event [/Users/aljabear/Projects/visualist/test/controllers/events_controller_test.rb:26]:
...
The GET request works fine. The POST request is borked. Printing out the #request.body after the POST request gives this clue:
{
"errors":[
{"title":"Bad Request",
"detail":"765: unexpected token at 'data[type]=events\u0026data[attributes][name]=Book+1\u0026data[attributes][body]=This+is+body+text.'",
"code":"400",
"status":"400"
}
]
}
So, clearly the :api_json content type is not being respected by Rails;
I guess it's instead spitting out form URL encoded.
if I do this instead, and print the result:
...
as: :json,
headers: {
'Content-type': 'application/vnd.api+json',
}
...
I get the following, showing that jsonapi-resources is behaving properly (just very strictly); when I use as: :json, Rails correctly formats things as json, just not when I do :api_json.
{
"errors":[{
"title":"Not acceptable",
"detail":"All requests must use the 'application/vnd.api+json' Accept without media type parameters. This request specified 'application/json'.",
"code":"406",
"status":"406"
}]
}
Is Rails just not bothering to convert the MIME type as requested? Or is this just a serialization issue? How can I force it to do it? Thanks... any clues are welcome.
I'm successfully using code like the following:
test 'should create event' do
assert_difference('Event.count') do
post events_url, params: {
data: {
type: 'events',
attributes: {
name: #event.name,
body: #event.body
}
}
},
as: :json,
headers: {
'Accept' => JSONAPI::MEDIA_TYPE,
'Content-Type' => JSONAPI::MEDIA_TYPE
}
end
assert_response :created
end
I have a project on GitHub that you can look at to see my code.
Is Rails just not bothering to convert the MIME type as requested? Or is this just a serialization issue? How can I force it to do it?
tl;dr: Yes, yes and as_json :)
tests are a subset of rails functionality - they don't go through the full rails stack (and thus does no conversions based on MIME type).
Yes, I think so - ie you need to serialize it so that rails can unserialize it.
Try running `as_json` on your data... eg
test 'should create event' do
assert_difference('Event.count') do
post events_url, params: {
data: {
type: 'events',
attributes: {
name: #event.name,
body: #event.body
}
}.as_json
},
as: :api_json
end
assert_response 201
end
or similar on your actual data... ie... if you're telling Rails to expect json... then pass it json? :D

Params missing error when sending raw data in JSON Post for RSpec

I am trying to send some raw data in a JSON post request to my RSpec controller test. I have successfully tested the controller with an actual Postman request but I can't get the Rspec test to work
The error I am getting is
param is missing or the value is empty: annotations
Here is my test set-up, which I have copied verbatimly from rails server log when I run the request via Postman
params = '{annotations"=>[{"id"=>1, "location_start"=>1, "location_end"=>3, "source_text"=>"what", "reading"=>"cool"}, {"id"=>2, "location_start"=>1, "location_end"=>-1, "reading"=>"cool"}]}'
patch :update, params, format: :json
I have also tried
params = {:annotations => [{ :id=>1, :location_start=>1, :location_end=>3}]}
patch :update, params.to_json
https://www.relishapp.com/rspec/rspec-rails/docs/controller-specs/controller-spec#setting-a-different-content-type-for-example-json-(request-type)
Check out how the json body is defined in this example: It's a ruby hash! So you need to not try passing in a raw string but a Hash that then gets automatically converted to JSON for you.
Hope that helps you out

How to post JSON data in rails 3 functional test

I plan to use JSON data in both request and response in my project and having some problems in testing.
After searching for a while, I find the following code which uses curl to post JSON data:
curl -H "Content-Type:application/json" -H "Accept:application/json" \
-d '{ "foo" : "bar" }' localhost:3000/api/new
In the controller I can access the JSON data simply using params[:foo] which is really easy. But for functional testing, I only find post and xhr (alias for xml_http_request).
How can I write functional test in rails to achieve the same effect as using curl? Or should I do test in other ways?
Here's what I've tried. I find the implementation for xhr in action_controller/test_case.rb, and tried to add jhr method simply changing 'Conetent-Type' and 'HTTP_ACCEPT'. (Added in test/test_helpers.rb.)
def json_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
#request.env['Content-Type'] = 'Application/json'
#request.env['HTTP_ACCEPT'] ||= [Mime::JSON, Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
__send__(request_method, action, parameters, session, flash).tap do
#request.env.delete 'Content-Type'
#request.env.delete 'HTTP_ACCEPT'
end
end
alias jhr :json_http_request
I used this in the same way as xhr, but it does not work. I inspected the #response object and sees the body is " ".
I also find one similar question on Stack Overflow but it's for rails 2 and the answer for posting raw data does not work in rails 3.
As of Rails 5, the way to do this is:
post new_widget_url, as: :json, params: { foo: "bar" }
This will also set the Content-type header correctly (to application/json).
I found that this does exactly what I want – post JSON to a controller's action.
post :create, {:format => 'json', :user => { :email => "test#test.com", :password => "foobar"}}
Just specify appropriate content type:
post :index, '{"foo":"bar", "bool":true}', "CONTENT_TYPE" => 'application/json'
Json data should go as a string, not as a Hash.
Looking at stack trace running a test you can acquire more control on request preparation:
ActionDispatch::Integration::RequestHelpers.post => ActionDispatch::Integration::Session.process =>
Rack::Test::Session.env_for
Specifying :format does not work because request go as 'application/x-www-form-urlencoded' and json isn't parsed properly processing a request body.
Assuming you have a controller named api, a method named new, and you're in the test for the api controller:
#request.env["RAW_POST_DATA"] = '{ "foo" : "bar" }'
post :new
did the trick for me.
Here is a snippet that let me post json data to test my own app. rails 3
port = Rails.env.production? ? 80 : 3000
uri = URI.parse( Rails.application.routes.url_helpers.books_url(:host => request.host, :port => port, :format => :json) )
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri)
request.content_type = 'application/json'
request.body = #json_data
response = http.request( request )
#result = response.body
Hope this helps others
As #taro suggests in a comment above, the syntax that works for me in functional and integration tests is:
post :create, {param1: 'value1', param2: 'value2', format: 'json'}
(The curly braces aren't always necessary, but sometimes it doesn't work if they're missing, so I always add them.)
Here's what params and request.format look like for a post of that sort:
params:
{"param1"=>"value1", "param2"=>"value2", "format"=>"json", "controller"=>"things", "action"=>"create"}
request.format:
application/json
The best answer I can come up with to this is you don't
Whether or not it was intentional it s maybe good that rails doesn't implement this for you.
In functional tests you really want to just test your controller and not rails method of deserialization or even that routing and mime detection are all setup correctly, those all fall under an IntegrationTest.
So for your controllers, don't pass JSON just pass your params hash like you normally would. Maybe adding :format as an argument as well if you need to check that and respond differently.
If you want to test the full stack move to an IntegrationTest

Resources