Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I fake uploading a file when testing Ruby Rack without the server?

For testing, I send a Rack::Request straight to the app, not using the server.

def request_via_API( app, method, path, params={} ) # app should be API
  env = Rack::MockRequest.env_for( path, {:method => method, :params=>params}  )
  app.handle Rack::Request.new(env)
end

works great for testing direct input, but I'm stymied by file upload. My real system works great from the browser with a file upload. But now I want to test it via the API, and don't know how to get the file contents into the request via any of the Rack classes/methods. (I tried making sense of Rack::Test::UploadedFile but didn't succeed).

Thanks, Alistair

like image 736
Alistair Cockburn Avatar asked Aug 24 '14 20:08

Alistair Cockburn


1 Answers

You were definitely on the right path. You can even use your function request_via_API without any modifications, e.g.:

request_via_API(app, 'POST', '/', {
  :field => "value",
  :text_source => Rack::Multipart::UploadedFile.new(PATH_TO_YOUR_FILE, MIME_TYPE_OF_YOUR_FILE)
})

This means you need to have some file somewhere. If you use fixtures, your test upload file should be around them. You can omit MIME time, but it defaults to text/plain.

If you use barebones Rack, you get the following hash after calling Rack::Multipart.parse_multipart:

{
  "field" => "value",
  "text_source" => {
    :filename => File.basename(PATH_TO_YOUR_FILE),
    :type => MIME_TYPE_OF_YOUR_FILE,
    :name => "text_source",
    :tempfile => Tempfile.new("RackMultipart"), # copied from PATH_TO_YOUR_FILE
    :head => "Content-Disposition: form-data; name=\"text_source\"; filename=\"#{File.basename(PATH_TO_YOUR_FILE)}\"\r\n" +
             "Content-Type: #{MIME_TYPE_OF_YOUR_FILE}\r\n" +
             "Content-Length: #{BYTESIZE_OF_YOUR_FILE}\r\n"
  }
}

The text_source key can have any other name, of course.

Rack::MockRequest#env_for automatically tries to create multipart form data request if:

  • HTTP method is not GET
  • there is no :input option provided
  • :params option is a Hash
  • :params option values contain at least one instance of Rack::Multipart::UploadedFile

You can see the details in the source code here and here.

I think relying on multipart request generation by Rack::MockRequest and Rack::Multipart is useful only for mocking HTML forms with file upload and file upload mechanisms that act the same. So, there's no need to use Rack::Multipart#build_multipart or Rack::Multipart::Generator directly.

If you have more complicated multipart scenarios or different file upload mechanism, you must pass opts argument with :input key instead of :params to Rack::MockRequest#env_for. How you generate that value for :input is your problem as far as Rack mocking capabilities are concerned. It only wraps it in StringIO if it is a String as you can see here. Otherwise, it is the same thing that will be passed as rack.input in the Rack environment hash and therefore it must conform to the Rack input stream spec (i.e., be an IO-like object).

Because this was also quite a challenge for me and I used it as an exercise to deepen my knowledge of Rack I created a simple project on GitHub to explore this file upload mocking.

Note: I tried to fix everything to Rack 1.5.2 except for the link to the Rack SPEC (so beware). Links to Ruby StdLib lead to the current version.

like image 177
F4-Z4 Avatar answered Sep 27 '22 22:09

F4-Z4