flask http-auth and unittesting
Asked Answered
T

2

6

Hi!

I have a route that I have protected using HTTP Basic authentication, which is implemented by Flask-HTTPAuth. Everything works fine (i can access the route) if i use curl, but when unit testing, the route can't be accessed, even though i provide it with the right username and password.

Here are the relevant code snippets in my testing module:

class TestClient(object):
    def __init__(self, app):
        self.client = app.test_client()

    def send(self, url, method, data=None, headers={}):
        if data:
            data = json.dumps(data)

        rv = method(url, data=data, headers=headers)
        return rv, json.loads(rv.data.decode('utf-8'))

    def delete(self, url, headers={}):
        return self.send(url, self.client.delete, headers)

class TestCase(unittest.TestCase):
    def setUp(self):
        app.config.from_object('test_config')
        self.app = app
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
        self.client = TestClient(self.app)

    def test_delete_user(self):
        # create new user
        data = {'username': 'john', 'password': 'doe'}
        self.client.post('/users', data=data)

        # delete previously created user
        headers = {}
        headers['Authorization'] = 'Basic ' + b64encode((data['username'] + ':' + data['password'])
                                                        .encode('utf-8')).decode('utf-8')
        headers['Content-Type'] = 'application/json'
        headers['Accept'] = 'application/json'
        rv, json = self.client.delete('/users', headers=headers)
        self.assertTrue(rv.status_code == 200) # Returns 401 instead

Here are the callback methods required by Flask-HTTPAuth:

auth = HTTPBasicAuth()

@auth.verify_password
def verify_password(username, password):
    # THIS METHOD NEVER GETS CALLED
    user = User.query.filter_by(username=username).first()
    if not user or not user.verify_password(password):
        return False
    g.user = user
    return True

@auth.error_handler
def unauthorized():
    response = jsonify({'status': 401, 'error': 'unauthorized', 'message': 'Please authenticate to access this API.'})
    response.status_code = 401
    return response

Any my route:

@app.route('/users', methods=['DELETE'])
@auth.login_required
def delete_user():
    db.session.delete(g.user)
    db.session.commit()
    return jsonify({})

The unit test throws the following exception:

Traceback (most recent call last):
  File "test_api.py", line 89, in test_delete_user
    self.assertTrue(rv.status_code == 200) # Returns 401 instead
AssertionError: False is not true

I want to emphazise once more that everything works fine when i run curl with exactly the same arguments i provide for my test client, but when i run the test, verify_password method doesn't even get called.

Thank you very much for your help!

Transfusion answered 16/1, 2015 at 11:39 Comment(5)
can you post the error message thrown by the unit test?Musclebound
Like i said, Flask-HTTPAuth redirects to unauthorized(), which returns a response object with a 401 status code. I have added the error message to my original question.Transfusion
Have you ensured that the POST request that adds the user really worked? Is that request exempt from authentication?Rockhampton
Yes, i am positive that adding the user worked, as i have tested this with previous unittests that i have not added to my original question for brevity. And yes, adding the user does not require authentication, my logic is that everyone can register a new account, but a registered account can only delete itself. Here's my entire api so far: github.com/stensoootla/spotpix_apiTransfusion
Maybe it's worth mentioning that when running the DELETE request with curl, the request.authorization object in delete_user() has the value {'password': 'doe', 'username': 'john'} and request.data has the value b'', but when i run the unit test (while removing the @login_required decorator from delete_user(), because otherwise the method is never called and thus i couldnt access the request object) , request.authorization is None and request.data has the value b'{"Authorization": "Basic am9objpkb2U=", "Content-Type": "application/json", "Accept": "application/json"}'Transfusion
R
2

You are going to love this.

Your send method:

def send(self, url, method, data=None, headers={}):
    pass

Your delete method:

def delete(self, url, headers={}):
    return self.send(url, self.client.delete, headers)

Note you are passing headers as third positional argument, so it's going as data into send().

Rockhampton answered 16/1, 2015 at 19:38 Comment(0)
T
3

Here is an example how this could be done with pytest and the inbuilt monkeypatch fixture.

If I have this API function in some_flask_app:

from flask_httpauth import HTTPBasicAuth

app = Flask(__name__)
auth = HTTPBasicAuth()

@app.route('/api/v1/version')
@auth.login_required
def api_get_version():
    return jsonify({'version': get_version()})

I can create a fixture that returns a flask test client and patches the authenticate function in HTTPBasicAuth to always return True:

import pytest
from some_flask_app import app, auth

@pytest.fixture(name='client')
def initialize_authorized_test_client(monkeypatch):
    app.testing = True
    client = app.test_client()
    monkeypatch.setattr(auth, 'authenticate', lambda x, y: True)
    yield client
    app.testing = False


def test_settings_tracking(client):
    r = client.get("/api/v1/version")
    assert r.status_code == 200
Turnbuckle answered 2/8, 2017 at 15:5 Comment(1)
Exactly what I needed.Pashalik
R
2

You are going to love this.

Your send method:

def send(self, url, method, data=None, headers={}):
    pass

Your delete method:

def delete(self, url, headers={}):
    return self.send(url, self.client.delete, headers)

Note you are passing headers as third positional argument, so it's going as data into send().

Rockhampton answered 16/1, 2015 at 19:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.