stages/email: add tests, cleanup
This commit is contained in:
parent
3219cffb52
commit
6676e95011
|
@ -12,7 +12,7 @@ class EmailStageSendForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
class EmailStageForm(forms.ModelForm):
|
class EmailStageForm(forms.ModelForm):
|
||||||
"""Form to create/edit Dummy Stage"""
|
"""Form to create/edit E-Mail Stage"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""email stage models"""
|
"""email stage models"""
|
||||||
from django.core.mail.backends.smtp import EmailBackend
|
from django.core.mail import get_connection
|
||||||
|
from django.core.mail.backends.base import BaseEmailBackend
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
@ -27,9 +28,9 @@ class EmailStage(Stage):
|
||||||
form = "passbook.stages.email.forms.EmailStageForm"
|
form = "passbook.stages.email.forms.EmailStageForm"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backend(self) -> EmailBackend:
|
def backend(self) -> BaseEmailBackend:
|
||||||
"""Get fully configured EMail Backend instance"""
|
"""Get fully configured EMail Backend instance"""
|
||||||
return EmailBackend(
|
return get_connection(
|
||||||
host=self.host,
|
host=self.host,
|
||||||
port=self.port,
|
port=self.port,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
|
|
|
@ -65,9 +65,4 @@ class EmailStageView(FormView, AuthenticationStage):
|
||||||
send_mails(self.executor.current_stage, message)
|
send_mails(self.executor.current_stage, message)
|
||||||
# We can't call stage_ok yet, as we're still waiting
|
# We can't call stage_ok yet, as we're still waiting
|
||||||
# for the user to click the link in the email
|
# for the user to click the link in the email
|
||||||
# return self.executor.stage_ok()
|
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
# def post(self, request: HttpRequest):
|
|
||||||
# """Just redirect to next stage"""
|
|
||||||
# return self.executor.()
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ def send_mails(stage: EmailStage, *messages: List[EmailMultiAlternatives]):
|
||||||
@CELERY_APP.task(
|
@CELERY_APP.task(
|
||||||
bind=True, autoretry_for=(SMTPException, ConnectionError,), retry_backoff=True
|
bind=True, autoretry_for=(SMTPException, ConnectionError,), retry_backoff=True
|
||||||
)
|
)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
|
def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
|
||||||
"""Send E-Mail according to EmailStage parameters from background worker.
|
"""Send E-Mail according to EmailStage parameters from background worker.
|
||||||
Automatically retries if message couldn't be sent."""
|
Automatically retries if message couldn't be sent."""
|
||||||
|
@ -38,6 +39,4 @@ def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
|
||||||
setattr(message_object, key, value)
|
setattr(message_object, key, value)
|
||||||
message_object.from_email = stage.from_address
|
message_object.from_email = stage.from_address
|
||||||
LOGGER.debug("Sending mail", to=message_object.to)
|
LOGGER.debug("Sending mail", to=message_object.to)
|
||||||
num_sent = stage.backend.send_messages([message_object])
|
stage.backend.send_messages([message_object])
|
||||||
if num_sent != 1:
|
|
||||||
raise self.retry()
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
<title>Simple Transactional Email</title>
|
<title></title>
|
||||||
<style>{% inline_static_ascii "stages/email/css/base.css" %}</style>
|
<style>{% inline_static_ascii "stages/email/css/base.css" %}</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
"""passbook core inlining template tags"""
|
"""passbook core inlining template tags"""
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
|
@ -10,21 +8,17 @@ register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inline_static_ascii(path: str) -> Optional[str]:
|
def inline_static_ascii(path: str) -> str:
|
||||||
"""Inline static asset. Doesn't check file contents, plain text is assumed"""
|
"""Inline static asset. Doesn't check file contents, plain text is assumed"""
|
||||||
result = finders.find(path)
|
result = finders.find(path)
|
||||||
if os.path.exists(result):
|
with open(result) as _file:
|
||||||
with open(result) as _file:
|
return _file.read()
|
||||||
return _file.read()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inline_static_binary(path: str) -> Optional[str]:
|
def inline_static_binary(path: str) -> str:
|
||||||
"""Inline static asset. Uses file extension for base64 block"""
|
"""Inline static asset. Uses file extension for base64 block"""
|
||||||
result = finders.find(path)
|
result = finders.find(path)
|
||||||
suffix = Path(path).suffix
|
suffix = Path(path).suffix
|
||||||
if os.path.exists(result):
|
with open(result) as _file:
|
||||||
with open(result) as _file:
|
return f"data:image/{suffix};base64," + _file.read()
|
||||||
return f"data:image/{suffix};base64," + _file.read()
|
|
||||||
return None
|
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""email tests"""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from django.core import mail
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
|
from passbook.core.models import Nonce, User
|
||||||
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
from passbook.stages.email.models import EmailStage
|
||||||
|
from passbook.stages.email.stage import QS_KEY_TOKEN
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailStage(TestCase):
|
||||||
|
"""Email tests"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username="unittest", email="test@beryju.org"
|
||||||
|
)
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
self.flow = Flow.objects.create(
|
||||||
|
name="test-email",
|
||||||
|
slug="test-email",
|
||||||
|
designation=FlowDesignation.AUTHENTICATION,
|
||||||
|
)
|
||||||
|
self.stage = EmailStage.objects.create(name="email",)
|
||||||
|
FlowStageBinding.objects.create(flow=self.flow, stage=self.stage, order=2)
|
||||||
|
|
||||||
|
def test_rendering(self):
|
||||||
|
"""Test with pending user"""
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
url = reverse(
|
||||||
|
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
|
)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_pending_user(self):
|
||||||
|
"""Test with pending user"""
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
url = reverse(
|
||||||
|
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
|
)
|
||||||
|
with self.settings(
|
||||||
|
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
||||||
|
):
|
||||||
|
response = self.client.post(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
self.assertEqual(mail.outbox[0].subject, "passbook - Password Recovery")
|
||||||
|
|
||||||
|
def test_token(self):
|
||||||
|
"""Test with token"""
|
||||||
|
# Make sure token exists
|
||||||
|
self.test_pending_user()
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
with patch("passbook.flows.views.FlowExecutorView.cancel", MagicMock()):
|
||||||
|
url = reverse(
|
||||||
|
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
|
)
|
||||||
|
token = Nonce.objects.get(user=self.user)
|
||||||
|
url += f"?{QS_KEY_TOKEN}={token.pk.hex}"
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, reverse("passbook_core:overview"))
|
||||||
|
|
||||||
|
session = self.client.session
|
||||||
|
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||||
|
self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER], self.user)
|
Reference in New Issue