# authentic2 - versatile identity manager
# Copyright (C) 2010-2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import json

import pytest
from django import VERSION as DJ_VERSION
from django.utils.html import escape
from webtest import Upload

from authentic2.a2_rbac.models import Role
from authentic2.apps.authenticators.models import AddRoleAction, BaseAuthenticator, LoginPasswordAuthenticator
from authentic2.models import Attribute
from authentic2_auth_fc.models import FcAuthenticator
from authentic2_auth_oidc.models import OIDCProvider
from authentic2_auth_saml.models import SAMLAuthenticator, SetAttributeAction

from .utils import assert_event, login, logout, request_select2


def test_authenticators_authorization(app, simple_user, simple_role, admin, superuser):
    simple_user.roles.add(simple_role.get_admin_role())  # grant user access to /manage/

    resp = login(app, simple_user, path='/manage/')
    assert 'Authenticators' not in resp.text
    app.get('/manage/authenticators/', status=403)

    role = Role.objects.get(name='Manager of authenticators')
    simple_user.roles.add(role)

    resp = app.get('/manage/')
    resp = resp.click('Authentication frontends')
    assert 'Authenticators' in resp.text

    logout(app)
    resp = login(app, admin, path='/manage/')
    assert 'Authentication frontends' in resp.text

    resp = resp.click('Authentication frontends')
    assert 'Authenticators' in resp.text

    logout(app)
    resp = login(app, superuser, path='/manage/')
    assert 'Authentication frontends' in resp.text

    resp = resp.click('Authentication frontends')
    assert 'Authenticators' in resp.text


def test_authenticators_password(app, superuser_or_admin, settings):
    resp = login(app, superuser_or_admin, path='/manage/authenticators/')
    # Password authenticator already exists
    assert 'Password' in resp.text
    authenticator = LoginPasswordAuthenticator.objects.get()

    resp = resp.click('Configure')
    assert 'Show condition: None' in resp.text
    # cannot delete password authenticator
    assert 'Delete' not in resp.text
    assert 'configuration is not complete' not in resp.text
    app.get('/manage/authenticators/%s/delete/' % authenticator.pk, status=403)

    resp = resp.click('Edit')
    assert list(resp.form.fields) == [
        'csrfmiddlewaretoken',
        'show_condition',
        'button_description',
        'registration_open',
        'min_password_strength',
        'password_min_length',
        'remember_me',
        'include_ou_selector',
        'password_regex',
        'password_regex_error_msg',
        'login_exponential_retry_timeout_duration',
        'login_exponential_retry_timeout_factor',
        'login_exponential_retry_timeout_max_duration',
        'login_exponential_retry_timeout_min_duration',
        'emails_ip_ratelimit',
        'sms_ip_ratelimit',
        'emails_address_ratelimit',
        'sms_number_ratelimit',
        None,
    ]

    resp.form['show_condition'] = '}'
    resp = resp.form.submit()
    assert 'could not parse expression: unmatched' in resp.text

    resp.form['show_condition'] = "'backoffice' in login_hint or remote_addr == '1.2.3.4'"
    resp = resp.form.submit().follow()
    assert 'Click "Edit" to change configuration.' not in resp.text
    if DJ_VERSION[0] <= 2:
        assert (
            'Show condition: &#39;backoffice&#39; in login_hint or remote_addr == &#39;1.2.3.4&#39;'
            in resp.text
        )
    else:
        # html-rendered quote characters change in django 3 onwards…
        assert (
            'Show condition: &#x27;backoffice&#x27; in login_hint or remote_addr == &#x27;1.2.3.4&#x27;'
            in resp.text
        )
    assert_event('authenticator.edit', user=superuser_or_admin, session=app.session)

    resp = resp.click('Edit')
    resp.form['show_condition'] = "remote_addr in dnsbl('ddns.entrouvert.org')"
    resp = resp.form.submit().follow()
    assert 'dnsbl' in resp.text

    # password authenticator cannot be disabled
    assert 'Disable' not in resp.text
    app.get('/manage/authenticators/%s/toggle/' % authenticator.pk, status=403)

    resp = resp.click('Journal of edits')
    assert resp.text.count('edit (show_condition)') == 2

    # cannot add another password authenticator
    resp = app.get('/manage/authenticators/add/')
    assert 'Password' not in resp.text

    # phone authn management feature flag is activated
    settings.A2_ALLOW_PHONE_AUTHN_MANAGEMENT = True

    phone1 = Attribute.objects.create(
        name='another_phone',
        kind='phone_number',
        label='Another phone',
    )
    phone2 = Attribute.objects.create(
        name='yet_another_phone',
        kind='fr_phone_number',
        label='Yet another phone',
    )

    resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)

    resp.form['accept_email_authentication'] = False
    resp.form['accept_phone_authentication'] = True
    assert resp.form['phone_identifier_field'].options == [
        (str(phone1.id), False, 'Another phone'),
        (str(phone2.id), False, 'Yet another phone'),
    ]
    resp.form['phone_identifier_field'] = phone2.id
    resp.form.submit()

    authenticator.refresh_from_db()
    assert authenticator.accept_email_authentication is False
    assert authenticator.accept_phone_authentication is True
    assert authenticator.phone_identifier_field == phone2


@pytest.mark.freeze_time('2022-04-19 14:00')
def test_authenticators_password_export(app, superuser):
    resp = login(app, superuser, path='/manage/authenticators/')
    assert LoginPasswordAuthenticator.objects.count() == 1

    resp = resp.click('Configure')
    resp = resp.click('Export')
    assert resp.headers['content-type'] == 'application/json'
    assert (
        resp.headers['content-disposition']
        == 'attachment; filename="export_password_authenticator_20220419.json"'
    )

    authenticator_json = json.loads(resp.text)
    assert authenticator_json == {
        'authenticator_type': 'authenticators.loginpasswordauthenticator',
        'name': '',
        'slug': 'password-authenticator',
        'show_condition': '',
        'button_description': '',
        'button_label': 'Login',
        'registration_open': True,
        'remember_me': None,
        'include_ou_selector': False,
        'min_password_strength': None,
        'password_min_length': 8,
        'password_regex': '',
        'password_regex_error_msg': '',
        'login_exponential_retry_timeout_duration': 1,
        'login_exponential_retry_timeout_factor': 1.8,
        'login_exponential_retry_timeout_max_duration': 3600,
        'login_exponential_retry_timeout_min_duration': 10,
        'emails_ip_ratelimit': '10/h',
        'sms_ip_ratelimit': '10/h',
        'emails_address_ratelimit': '3/d',
        'sms_number_ratelimit': '10/h',
        'ou': None,
        'related_objects': [],
        'accept_email_authentication': True,
        'accept_phone_authentication': False,
    }

    resp = app.get('/manage/authenticators/')
    resp = resp.click('Import')
    authenticator_json['button_description'] = 'test'
    resp.form['authenticator_json'] = Upload(
        'export.json', json.dumps(authenticator_json).encode(), 'application/json'
    )
    resp = resp.form.submit()
    assert LoginPasswordAuthenticator.objects.count() == 1
    assert LoginPasswordAuthenticator.objects.get().button_description == 'test'


def test_authenticators_fc(app, superuser):
    resp = login(app, superuser, path='/manage/authenticators/')

    resp = resp.click('Add new authenticator')
    resp.form['authenticator'] = 'fc'
    resp = resp.form.submit()
    assert '/edit/' in resp.location

    provider = FcAuthenticator.objects.get()
    assert provider.order == -1

    resp = app.get(provider.get_absolute_url())
    assert 'extra-actions-menu-opener' in resp.text
    assert 'Platform: Integration' in resp.text
    assert 'Scopes: profile (profile), email (email)' in resp.text
    assert 'Client ID' not in resp.text
    assert 'Client Secret' not in resp.text

    resp = resp.click('Edit')
    assert list(resp.form.fields) == [
        'csrfmiddlewaretoken',
        'show_condition',
        'platform',
        'client_id',
        'client_secret',
        'scopes',
        'link_by_email',
        None,
    ]
    assert 'phone' not in resp.pyquery('#id_scopes').html()
    assert 'address' not in resp.pyquery('#id_scopes').html()

    resp.form['platform'] = 'prod'
    resp.form['client_id'] = '211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6k'
    resp.form['client_secret'] = '211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6d'
    resp.form['scopes'] = ['given_name', 'birthdate']
    resp = resp.form.submit().follow()

    assert 'Platform: Production' in resp.text
    assert 'Scopes: given name (given_name), birthdate (birthdate)' in resp.text
    assert 'Client ID: 211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6k' in resp.text
    assert 'Client Secret: 211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6d' in resp.text

    resp = app.get('/manage/authenticators/')
    assert 'FranceConnect' in resp.text
    assert 'class="section disabled"' in resp.text

    resp = resp.click('Configure', index=1)
    resp = resp.click('Enable').follow()
    assert 'Authenticator has been enabled.' in resp.text

    resp = app.get('/manage/authenticators/')
    assert 'class="section disabled"' not in resp.text

    provider.refresh_from_db()
    provider.scopes.extend(['phone', 'address'])  # deprecated scopes
    provider.save()

    resp = app.get(provider.get_absolute_url())
    resp = resp.click('Edit')
    resp.form.submit().follow()
    provider.refresh_from_db()
    assert 'phone' not in provider.scopes
    assert 'address' not in provider.scopes


def test_authenticators_saml(app, superuser, ou1, ou2):
    resp = login(app, superuser, path='/manage/authenticators/')

    resp = resp.click('Add new authenticator')
    resp.form['name'] = 'Test'
    resp.form['authenticator'] = 'saml'
    resp = resp.form.submit()

    authenticator = SAMLAuthenticator.objects.filter(slug='test').get()
    resp = app.get(authenticator.get_absolute_url())
    assert 'Create user if their username does not already exists: Yes' in resp.text
    assert 'Metadata file path' not in resp.text

    assert 'Enable' not in resp.text
    assert 'configuration is not complete' in resp.text

    resp = resp.click('Edit')
    assert resp.pyquery('button#tab-general').attr('class') == 'pk-tabs--button-marker'
    assert not resp.pyquery('button#tab-advanced').attr('class')
    assert 'ou' in resp.form.fields

    resp = resp.form.submit()
    assert 'One of the metadata fields must be filled.' in resp.text

    resp.form['metadata_url'] = 'https://example.com/metadata.xml'
    resp = resp.form.submit().follow()
    assert 'Metadata URL: https://example.com/metadata.xml' in resp.text

    resp = resp.click('Enable').follow()
    assert 'Authenticator has been enabled.' in resp.text

    resp = resp.click('Edit')
    resp.form['attribute_mapping'] = '[{"attribute": "email", "saml_attribute": "mail", "mandatory": false}]'
    resp = resp.form.submit().follow()

    authenticator.refresh_from_db()
    assert authenticator.attribute_mapping == [
        {'attribute': 'email', 'saml_attribute': 'mail', 'mandatory': False}
    ]

    resp = resp.click('Edit')
    assert resp.pyquery('button#tab-advanced').attr('class') == 'pk-tabs--button-marker'

    resp = app.get(authenticator.get_absolute_url())
    resp = resp.click('Journal of edits')
    assert 'edit (metadata_url)' in resp.text


def test_authenticators_saml_hide_metadata_url_advanced_fields(app, superuser, ou1, ou2):
    authenticator = SAMLAuthenticator.objects.create(slug='idp1')

    resp = login(app, superuser)
    resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)
    assert 'Metadata cache time' not in resp.text
    assert 'Metadata HTTP timeout' not in resp.text

    resp.form['metadata_url'] = 'https://example.com/metadata.xml'
    resp = resp.form.submit().follow()

    resp = resp.click('Edit')
    assert 'Metadata cache time' in resp.text
    assert 'Metadata HTTP timeout' in resp.text


def test_authenticators_saml_validate_metadata(app, superuser):
    authenticator = SAMLAuthenticator.objects.create(slug='idp1')

    resp = login(app, superuser)
    resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)
    resp.form['metadata'] = 'invalid'

    resp.form['metadata'] = '<a/>'
    resp = resp.form.submit()
    assert 'Invalid metadata, missing tag {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor' in resp.text

    resp.form[
        'metadata'
    ] = '<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata"></ns0:EntityDescriptor>'
    resp = resp.form.submit()
    assert 'Invalid metadata, missing entityID' in resp.text

    resp.form['metadata'] = (
        '<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata"'
        ' entityID="https://example.com"></ns0:EntityDescriptor>'
    )
    resp.form.submit(status=302)


def test_authenticators_saml_empty_attribute_mapping(app, superuser):
    authenticator = SAMLAuthenticator.objects.create(metadata_url='https://example.com/meta.xml', slug='idp1')

    resp = login(app, superuser)
    resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)

    resp.form['attribute_mapping'] = None
    resp = resp.form.submit().follow()

    authenticator.refresh_from_db()
    assert authenticator.attribute_mapping == {}


def test_authenticators_saml_view_metadata(app, superuser):
    authenticator = SAMLAuthenticator.objects.create(slug='idp1')

    resp = login(app, superuser)
    resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)

    assert 'Metadata (XML):' not in resp.text
    assert app.get('/manage/authenticators/%s/metadata.xml' % authenticator.pk, status=404)

    authenticator.metadata = '<a><b></b></a>'
    authenticator.save()

    resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
    assert 'Metadata (XML):' in resp.text

    resp = resp.click('View metadata')
    assert resp.text == '<a><b></b></a>'


def test_authenticators_saml_missing_signing_key(app, superuser, settings):
    authenticator = SAMLAuthenticator.objects.create(slug='idp1')

    resp = login(app, superuser)
    resp = app.get(authenticator.get_absolute_url())
    assert 'Signing key is missing' in resp.text

    settings.MELLON_PRIVATE_KEY = 'xxx'
    settings.MELLON_PUBLIC_KEYS = ['yyy']
    resp = app.get(authenticator.get_absolute_url())
    assert 'Signing key is missing' not in resp.text


def test_authenticators_saml_no_name_display(app, superuser, ou1, ou2):
    SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')

    resp = login(app, superuser, path='/manage/authenticators/')
    assert 'SAML - idp1' in resp.text


def test_authenticators_saml_name_id_format_select(app, superuser):
    authenticator = SAMLAuthenticator.objects.create(metadata_url='https://example.com/meta.xml', slug='idp1')

    resp = login(app, superuser, path='/manage/authenticators/%s/edit/' % authenticator.pk)
    resp.form['name_id_policy_format'].select(
        text='Persistent (urn:oasis:names:tc:SAML:2.0:nameid-format:persistent)'
    )
    resp.form.submit().follow()

    authenticator.refresh_from_db()
    assert authenticator.name_id_policy_format == 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'


def test_authenticators_saml_attribute_lookup(app, superuser):
    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
    resp = login(app, superuser, path=authenticator.get_absolute_url())

    resp = resp.click('Add', href='samlattributelookup')
    resp.form['user_field'].select(text='Email address (email)')
    resp.form['saml_attribute'] = 'mail'
    resp = resp.form.submit()
    assert_event('authenticator.related_object.creation', user=superuser, session=app.session)
    assert '#open:samlattributelookup' in resp.location

    resp = resp.follow()
    assert escape('"mail" (from "Email address (email)")') in resp.text

    resp = resp.click('mail')
    resp.form['ignore_case'] = True
    resp = resp.form.submit().follow()
    assert escape('"mail" (from "Email address (email)"), case insensitive') in resp.text
    assert_event('authenticator.related_object.edit', user=superuser, session=app.session)

    Attribute.objects.create(kind='string', name='test', label='Test')
    resp = resp.click('mail')
    resp.form['user_field'].select(text='Test (test)')
    resp = resp.form.submit().follow()
    assert escape('"mail" (from "Test (test)"), case insensitive') in resp.text

    resp = resp.click('Remove', href='samlattributelookup')
    resp = resp.form.submit().follow()
    assert 'Test (test)' not in resp.text
    assert_event('authenticator.related_object.deletion', user=superuser, session=app.session)


def test_authenticators_saml_set_attribute(app, superuser):
    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
    resp = login(app, superuser, path=authenticator.get_absolute_url())

    resp = resp.click('Add', href='setattributeaction')
    resp.form['user_field'].select(text='Email address (email)')
    resp.form['saml_attribute'] = 'mail'
    resp = resp.form.submit().follow()
    assert escape('"Email address (email)" from "mail"') in resp.text

    resp = resp.click('mail')
    resp.form['mandatory'] = True
    resp = resp.form.submit().follow()
    assert escape('"Email address (email)" from "mail" (mandatory)') in resp.text


def test_authenticators_saml_add_role(app, superuser, role_ou1, role_ou2):
    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
    resp = login(app, superuser, path=authenticator.get_absolute_url())

    resp = resp.click('Add', href='addroleaction')
    select2_json = request_select2(app, resp, term='role_ou')
    assert [x['text'] for x in select2_json['results']] == ['OU1 - role_ou1', 'OU2 - role_ou2']

    resp.form['role'].force_value(select2_json['results'][0]['id'])
    resp = resp.form.submit().follow()
    assert 'role_ou1' in resp.text

    resp = resp.click('role_ou1')
    resp.form['role'].force_value(select2_json['results'][1]['id'])
    resp = resp.form.submit().follow()
    assert 'role_ou1' not in resp.text
    assert 'role_ou2' in resp.text


def test_authenticators_saml_export(app, superuser, simple_role):
    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
    SetAttributeAction.objects.create(authenticator=authenticator, user_field='test', saml_attribute='hop')
    AddRoleAction.objects.create(authenticator=authenticator, role=simple_role)

    resp = login(app, superuser, path=authenticator.get_absolute_url())
    export_resp = resp.click('Export')

    SAMLAuthenticator.objects.all().delete()
    SetAttributeAction.objects.all().delete()
    AddRoleAction.objects.all().delete()

    resp = app.get('/manage/authenticators/import/')
    resp.form['authenticator_json'] = Upload('export.json', export_resp.body, 'application/json')
    resp = resp.form.submit().follow()

    authenticator = SAMLAuthenticator.objects.get()
    assert authenticator.slug == 'idp1'
    assert authenticator.metadata == 'meta1.xml'
    assert SetAttributeAction.objects.filter(
        authenticator=authenticator, user_field='test', saml_attribute='hop'
    ).exists()
    assert AddRoleAction.objects.filter(authenticator=authenticator, role=simple_role).exists()


def test_authenticators_order(app, superuser):
    resp = login(app, superuser, path='/manage/authenticators/')

    saml_authenticator = SAMLAuthenticator.objects.create(name='Test', slug='test', enabled=True, order=42)
    SAMLAuthenticator.objects.create(name='Test disabled', slug='test-disabled', enabled=False)
    fc_authenticator = FcAuthenticator.objects.create(slug='fc-authenticator', enabled=True, order=-1)
    password_authenticator = LoginPasswordAuthenticator.objects.get()

    assert fc_authenticator.order == -1
    assert password_authenticator.order == 0
    assert saml_authenticator.order == 42

    resp = resp.click('Edit order')
    assert resp.text.index('FranceConnect') < resp.text.index('Password') < resp.text.index('SAML - Test')
    assert 'SAML - Test disabled' not in resp.text

    resp.form['order'] = '%s,%s,%s' % (saml_authenticator.pk, password_authenticator.pk, fc_authenticator.pk)
    resp.form.submit()

    fc_authenticator.refresh_from_db()
    password_authenticator.refresh_from_db()
    saml_authenticator.refresh_from_db()
    assert fc_authenticator.order == 2
    assert password_authenticator.order == 1
    assert saml_authenticator.order == 0


def test_authenticators_add_last(app, superuser):
    resp = login(app, superuser, path='/manage/authenticators/')

    BaseAuthenticator.objects.all().delete()

    resp = resp.click('Add new authenticator')
    resp.form['name'] = 'Test'
    resp.form['authenticator'] = 'saml'
    resp.form.submit()

    authenticator = SAMLAuthenticator.objects.get()
    assert authenticator.order == 1

    authenticator.order = 42
    authenticator.save()
    resp = app.get('/manage/authenticators/add/')
    resp.form['name'] = 'Test 2'
    resp.form['authenticator'] = 'saml'
    resp.form.submit()

    authenticator = SAMLAuthenticator.objects.filter(slug='test-2').get()
    assert authenticator.order == 43


def test_authenticators_configuration_info(app, superuser, ou1, ou2):
    resp = login(app, superuser, path='/manage/authenticators/')

    resp = resp.click('Add new authenticator')
    assert resp.text.count('infonotice') == 2
    assert '<div class="infonotice saml idp-info">' in resp.text
    assert '<div class="infonotice oidc idp-info">' in resp.text
    assert resp.text.count('Configuration information for your identity provider') == 2

    # saml
    assert (
        'Metadata URL:<br><a href="https://testserver/accounts/saml/metadata/" rel="nofollow">'
        'https://testserver/accounts/saml/metadata/</a>'
    ) in resp.text
    assert 'Commonly expected attributes:' in resp.text
    assert 'Email (email)' in resp.text

    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
    SetAttributeAction.objects.create(
        authenticator=authenticator, user_field='email', saml_attribute='mail', mandatory=True
    )
    SetAttributeAction.objects.create(
        authenticator=authenticator, user_field='first_name', saml_attribute='given_name'
    )

    resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
    assert 'Information for configuration' in resp.text
    assert 'Expected attributes' in resp.text
    assert 'mail (mandatory)' in resp.text
    assert 'given_name' in resp.text

    # oidc
    authenticator = OIDCProvider.objects.create(slug='idp2')
    for url in ('/manage/authenticators/add/', '/manage/authenticators/%s/detail/' % authenticator.pk):
        resp = app.get(url)
        assert (
            'Redirect URI (redirect_uri):<br><a href="https://testserver/accounts/oidc/callback/" '
            'rel="nofollow">https://testserver/accounts/oidc/callback/</a>'
        ) in resp.text
        assert (
            'Redirect URI after logout (post_logout_redirect_uri):<br><a href="https://testserver/logout/" '
            'rel="nofollow">https://testserver/logout/</a>'
        ) in resp.text


def test_authenticators_journal_pages(app, superuser):
    authenticator = LoginPasswordAuthenticator.objects.get()

    # generate login failure event
    login(app, 'noone', password='wrong', fail=True)

    login(app, superuser)
    resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)

    # generate edit event
    resp.form['button_description'] = 'abc'
    resp = resp.form.submit().follow()

    resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
    resp = resp.click('Journal of edits')
    assert resp.pyquery('td.journal-list--message-column').text() == 'edit (button_description)'
    assert 'noone' not in resp.text

    resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
    resp = resp.click('Journal of logins')
    assert resp.pyquery('td.journal-list--message-column').text() == 'login failure with username "noone"'
    assert 'edit (button_description)' not in resp.text
