Charles R
Charles R

Reputation: 1661

Flask Testing - Dynamically test all protected routes from a blueprint

I would like to test that all routes from a blueprint are protected with a login required decorator.

The point is : If a developer add a new route and forget to add this decorator, I would like my test to automatically spot that lack.

In order to do that, I would like to loop trough all routes and methods

for rule in app.url_map.iter_rules():
    if rule.endpoint.startswith("my_blueprint"):
        response = app.test_client().get(rule)
        assert response.status_code == 401

As you can see, I have to specify the method (get, post..) like this app.test_client().get(rule).

Is there a more dynamic way to to invoke the methods ?

Upvotes: 0

Views: 621

Answers (1)

chuckles
chuckles

Reputation: 761

Discovery function

def blueprint_site_map(app, blueprint, all_methods=False):
    '''
    utilizes Flask's built-in rule mapper to generate a
    site-map of the application, returning a list of dicts, ex.
    {   
        'endpoint'  :   repr(Blueprint)
        'methods'   :   list
        'rule'      :   /route
    {
    '''
    reply = []
    rules = list(app.url_map.iter_rules())
    ignored_methods = set(() if all_methods else ('HEAD', 'OPTIONS'))
    rule_methods = [','.join(sorted(rule.methods - ignored_methods)) for rule in rules]
    for rule, methods in zip(rules, rule_methods):
        if (rule.endpoint != 'static') and (rule.endpoint.startswith(blueprint)):
            reply.append(dict(endpoint=rule.endpoint, methods=methods.split(','), rule=rule.rule))
    return reply

Sample output

>>> blueprint_site_map(app, 'my_blueprint')
[
  {
    'endpoint': 'my_blueprint.foo',
    'methods': ['GET', 'POST'],
    'rule': '/auth/foo'
  },
  {
    'endpoint': 'my_blueprint.bar',
    'methods': ['DELETE', 'GET', 'POST'],
    'rule': '/auth/bar'
  }
]

Usage

def test_my_blueprint_is_protected(client):
    from flask import current_app as app
    obj = blueprint_site_map(app, 'my_blueprint')
    for each in obj:
        for method in each['methods']:
            func = getattr(client, method)
            url = each['rule']  # *see note
            kwargs = {}         # inject headers, etc if needed
            response = func(url, **kwargs)
            assert response.status_code == 401

It should be noted that if you are using any parameterized URL rules, such as allowing both /foo and /foo/<string:s> then you will need to manually template or filter these out. The blueprint_site_map function will include separate list elements for /foo and /foo/<string:s>, which when taken literally will cause problems either from the test client itself, or with your route logic.

The discovery function is crafted in such a way that you can use this convention for as many different blueprints as necessary, which by very nature of your using the Blueprint convention means that you can keep your unit tests as modular as the app.

Cheers!

Upvotes: 3

Related Questions