pythonfile-uploadfunctional-testingfalconframework

Simulate multipart/form-data file upload with Falcon's Testing module


This simple Falcon API will take a HTTP POST with enctype=multipart/form-data and a file upload in the file parameter and print the file's content on the console:

# simple_api.py
import cgi
import falcon

class SomeTestApi(object):
    def on_post(self, req, resp):
        upload = cgi.FieldStorage(fp=req.stream, environ=req.env)
        upload = upload['file'].file.read()
        print(upload)


app = falcon.API()
app.add_route('/', SomeTestApi())

One might also use the falcon-multipart middleware to achieve the same goal.

To try it out, run it e.g. with gunicorn (pip install gunicorn),

gunicorn simple_api.py

then use cUrl (or any REST client of choice) to upload a text file:

# sample.txt
this is some sample text

curl -F "file=@sample.txt" localhost:8000

I would like to test this API now with Falcon's testing helpers by simulating a file upload. However, I do not understand yet how to do this (if it is possible at all?). The simulate_request method has a file_wrapper parameter which might be useful but from the documentation I do not understand how this is supposed to be filled.

Any suggestions?


Solution

  • This is what I came up with, which tries to simulate what my Chrome does. Note that this simulates the case when you are uploading only one file, but you can simply modify this function to upload multiple files, each one separated by two new lines.

    def create_multipart(data, fieldname, filename, content_type):
        """
        Basic emulation of a browser's multipart file upload
        """
        boundry = '----WebKitFormBoundary' + random_string(16)
        buff = io.BytesIO()
        buff.write(b'--')
        buff.write(boundry.encode())
        buff.write(b'\r\n')
        buff.write(('Content-Disposition: form-data; name="%s"; filename="%s"' % \
                   (fieldname, filename)).encode())
        buff.write(b'\r\n')
        buff.write(('Content-Type: %s' % content_type).encode())
        buff.write(b'\r\n')
        buff.write(b'\r\n')
        buff.write(data)
        buff.write(b'\r\n')
        buff.write(boundry.encode())
        buff.write(b'--\r\n')
        headers = {'Content-Type': 'multipart/form-data; boundary=%s' %boundry}
        headers['Content-Length'] = str(buff.tell())
        return buff.getvalue(), headers
    

    You can then use this function like the following:

    with open('test/resources/foo.pdf', 'rb') as f:
        foodata = f.read()
    
    # Create the multipart data
    data, headers = create_multipart(foodata, fieldname='uploadFile',
                                     filename='foo.pdf',
                                     content_type='application/pdf')
    
    # Post to endpoint
    client.simulate_request(method='POST', path=url,
                            headers=headers, body=data)