"""E-mail change: request, confirmation link, undo link, limits, sessions, audit and logs.""" import logging import re from datetime import timedelta import pytest from django.core.management import call_command from django.test import Client from django.utils import timezone from portal.accounts.models import ThrottleCounter, UserSession from portal.adapters.directory import DirectoryUnavailable, ValidationFailed from portal.audit.models import AuditEntry from portal.mailer.models import OutboxMail from portal.profile import services from portal.profile.models import PendingEmailChange from portal.tests.helpers import sign_in pytestmark = pytest.mark.django_db(transaction=True) CURRENT = "Correct-Horse-9" WRONG = "Not-The-Password-1" OLD = "alice@example.test" NEW = "alice.new@example.test" ADMIN = "admin@example.test" LINK = re.compile(r"https://portal\.example\.test(/email/(?:confirm|undo)/[\w-]+)") DEAD = "This link is not valid any more." @pytest.fixture(autouse=True) def admin_address(settings): settings.ADMIN_NOTIFY_EMAIL = ADMIN @pytest.fixture def people(adapters): d = adapters.directory d.add_user("alice", password=CURRENT, display_name="Alice Anders", mail=OLD) d.add_user("bob", display_name="Bob Brown", mail="bob@example.test") d.add_user("nomail", password=CURRENT, display_name="No Mail") return d class Spy: def __init__(self, directory): self.calls = [] for name in ("password_policy", "verify_password", "find_user", "update_attributes"): self._wrap(directory, name) def _wrap(self, directory, name): original = getattr(directory, name) def wrapper(*args, **kwargs): self.calls.append(name) return original(*args, **kwargs) setattr(directory, name, wrapper) def signed_in(adapters, name="alice"): client, _ = sign_in(adapters, name) return client def ask(client, new=NEW, password=CURRENT): return client.post("/email", {"new_email": new, "current_password": password}) def mails(to=None): rows = OutboxMail.objects.all() return list(rows.filter(recipient=to) if to else rows) def link_in(mail): return LINK.search(mail.body_text).group(1) def request_link(adapters, name="alice", new=NEW): """Sign in, ask for a change and return the confirmation path.""" client = signed_in(adapters, name) assert ask(client, new).status_code == 302 return link_in(mails(new)[-1]) def confirmed_undo_path(adapters): path = request_link(adapters) Client().post(path) return link_in(mails(OLD)[-1]) def actions(): names = AuditEntry.objects.values_list("action", flat=True) return [name for name in names if name.startswith("email.")] def mail_of(adapters, name="alice"): return adapters.directory.find_user(name).mail # -- the page ----------------------------------------- def test_the_page_shows_the_current_address_and_is_reached_from_the_profile(people, adapters): client = signed_in(adapters) profile = client.get("/profile").content.decode() assert 'href="/email"' in profile and "Change e-mail address" in profile html = client.get("/email").content.decode() assert OLD in html and 'name="new_email"' in html and 'name="current_password"' in html assert 'autocomplete="current-password"' in html and "" assert redact("Internal Server Error: /email/undo/abc") == ( "Internal Server Error: /email/undo/" ) assert redact("/password") == "/password" def test_the_configured_handler_blanks_tokens_from_every_logger(): import io import logging.config from portal.config.logging import build_logging root = logging.getLogger() saved = (root.handlers[:], root.level) request_logger = logging.getLogger("django.request") saved_request = (request_logger.handlers[:], request_logger.level) try: logging.config.dictConfig(build_logging("INFO")) handler = root.handlers[0] stream = io.StringIO() handler.setStream(stream) logging.getLogger("django.security.csrf").warning( "Forbidden (CSRF token missing): /email/confirm/SECRET-token_123" ) output = stream.getvalue() finally: root.handlers[:], root.level = saved request_logger.handlers[:], request_logger.level = saved_request assert "SECRET-token_123" not in output and "/email/confirm/" in output