David J.
David J.

Reputation: 1913

flask test client returns "method not allowed" for requests even though the web client works

I recently refactored my flask app to separate the views from the init file.

my structure is like this:

api
  __init__.py
  views.py

My app was initially like this:

import os

from flask import Flask


def create_app(test_config=None):
  # create and configure the app
    app = Flask(__name__)
    setup_db(app)
    CORS(app)

  # CORS Headers 
    @app.after_request
    def after_request(response):
        """Docstring for my function"""
        response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization,true')
        response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
        return response
  
    # my views were all contained below
    @app.route('/hello')
    def hello():
        return 'Hello, World!'  

    return app

And then I refactored to be defined elsewhere and referenced in create_app:

api/init.py

from .views import hello
def create_app():
    ...
    app.route('/hello')(hello)
    app.route('/hello', methods=['POST'])(create_hello)    

api/views.py

def hello():
    return 'Hello, World!'

def create_hello():
    return 'something else'

I have a pytest file that tests the creation of a hello:

  @pytest.fixture()
    def set_up():
        app = create_app()
        client = app.test_client
        database_path = "postgresql://{}:{}@{}/{}".format(username, password,'localhost:5432', test_db_name)
        setup_db(app, database_path)
    
        with app.app_context():
            db = SQLAlchemy()
            db.init_app(app)
            db.create_all()
        return client        
    
    
    def test_create_hello(set_up):
        client = set_up()
        res = client.post('/hello', json={"name":"jim-bob")
        data = json.loads(res.data)        
        assert res.status_code == 200
        assert data['success'] == True

When I run pytest, I get a similar error for each of my methods:

    def test_create_hello(set_up):
        client = set_up()
        res = client.post('/hello', json={'name':'jim-bob')
>       data = json.loads(res.data)

test_api.py:128: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/lib/python3.6/json/__init__.py:354: in loads
    return _default_decoder.decode(s)
/usr/lib/python3.6/json/decoder.py:339: in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <json.decoder.JSONDecoder object at 0x7f069b43dc18>
s = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>405 Method Not Allowed</title>\n<h1>Method Not Allowed</h1>\n<p>The method is not allowed for the requested URL.</p>\n'
idx = 0

    def raw_decode(self, s, idx=0):
        """Decode a JSON document from ``s`` (a ``str`` beginning with
        a JSON document) and return a 2-tuple of the Python
        representation and the index in ``s`` where the document ended.
    
        This can be used to decode a JSON document from a string that may
        have extraneous data at the end.
    
        """
        try:
            obj, end = self.scan_once(s, idx)
        except StopIteration as err:
>           raise JSONDecodeError("Expecting value", s, err.value) from None
E           json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

My api still works in the browser or using curl, but the tests no longer work for some reason.

I've only had this problem since I refactored my views outside of my create_app function. Can anyone see what could be causing this?

Upvotes: 2

Views: 969

Answers (3)

rdas
rdas

Reputation: 21285

@app.route('/hello')

this only binds the GET method to the handler.

Yet here:

res = client.post('/hello', json={'name': 'jim-bob'})

you are doing a POST

To make the same handler handle POST requests as well, do:

@app.route('/hello', methods=['GET', 'POST'])

or add a different handler for POST requests. See documentation.

Upvotes: 1

Ryabchenko Alexander
Ryabchenko Alexander

Reputation: 12420

you need to rewrite your code like

@app.route('/hello', methods=['GET', 'POST'])
def hello():
    if flask.request.method == 'POST':
       ...
    else:  # GET
       ...

Upvotes: 0

abhishek kasana
abhishek kasana

Reputation: 1522

As @hoefing pointed out the response returned from the "/hello" route method is just a string. That response would be wrapped in the flask base Response class which by default has content-type as text/HTML. Hence your response caught and loaded as JSON by the test function faces an error in decoding.

The test you have added should handle the response according to the content-type of response returned which in this case is not a JSON.

#solutions to fix the issue

  1. (solution 1) Update your response for the "/hello" route. For this, you can either initialize flask Response class yourself with the content-type header or use flask jsonify which does that automatically.

    from flask import make_response, jsonify
    
    def hello():
       #headers = {'Content-Type': 'application/json'}
       #return make_response('Hello, World!', 200, headers)
       return jsonify('Hello, World!')
    
  2. (solution 2) Update the test logic to handle the text/HTML response.

    def test_create_hello(set_up):
       client = set_up()
       res = client.post('/hello', json={'name':'jim-bob')
       data = res.data
       # handle this html text data
    

Upvotes: 0

Related Questions