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

Question:

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 "[email protected]" 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?

Asked By: Dirk

||

Answers:

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'rn')
    buff.write(('Content-Disposition: form-data; name="%s"; filename="%s"' % 
               (fieldname, filename)).encode())
    buff.write(b'rn')
    buff.write(('Content-Type: %s' % content_type).encode())
    buff.write(b'rn')
    buff.write(b'rn')
    buff.write(data)
    buff.write(b'rn')
    buff.write(boundry.encode())
    buff.write(b'--rn')
    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)
Answered By: PoP

You can craft a suitable request body and Content-Type using the encode_multipart_formdata function in urllib3, documented here. An example usage:

from falcon import testing
import pytest
import myapp
import urllib3

# Depending on your testing strategy and how your application
# manages state, you may be able to broaden the fixture scope
# beyond the default 'function' scope used in this example.

@pytest.fixture()
def client():
    # Assume the hypothetical `myapp` package has a function called
    # `create()` to initialize and return a `falcon.App` instance.
    return testing.TestClient(myapp.create())


# a dictionary mapping the HTML form label to the file uploads
fields = {
        'file_1_form_label': ( # label in HTML form object
            'file1.txt',  # filename
            open('path/to/file1.txt').read(),  # file contents
            'text/plain'  # MIME type
        ),
        'file_2_form_label': (
            'file2.json',
            open('path/to/file2.json').read(),
            'application/json'
        )
    }

# create the body and header
body, content_type_header = urllib3.encode_multipart_formdata(fields)

# NOTE: modify these headers to reflect those generated by your browser
# and/or required by the falcon application you're testing
headers = {
        'Content-Type': content_type_header,
    }

# craft the mock query using the falcon testing framework
response = client.simulate_request(
    method="POST",
    path='/app_path',
    headers=headers,
    body=body)

print(response.status_code)

Note the syntax of the fields object, which is used as input for the encode_multipart_formdata function.
See Tim Head’s blog post for another example:
https://betatim.github.io/posts/python-create-multipart-formdata/
Falcon testing example copied from their docs:
https://falcon.readthedocs.io/en/stable/api/testing.html

Answered By: jppotGSTVe8