stages/identification: explicitly define enrollment and recovery
This commit is contained in:
parent
8b4558fcd0
commit
4d1658b35e
|
@ -1,13 +1,15 @@
|
||||||
"""flow planner tests"""
|
"""flow planner tests"""
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
|
from passbook.core.models import User
|
||||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.flows.planner import FlowPlanner
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||||
from passbook.policies.types import PolicyResult
|
from passbook.policies.types import PolicyResult
|
||||||
from passbook.stages.dummy.models import DummyStage
|
from passbook.stages.dummy.models import DummyStage
|
||||||
|
|
||||||
|
@ -81,3 +83,24 @@ class TestFlowPlanner(TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
TIME_NOW_MOCK.call_count, 2
|
TIME_NOW_MOCK.call_count, 2
|
||||||
) # When taking from cache, time is not measured
|
) # When taking from cache, time is not measured
|
||||||
|
|
||||||
|
def test_planner_default_context(self):
|
||||||
|
"""Test planner with default_context"""
|
||||||
|
flow = Flow.objects.create(
|
||||||
|
name="test-default-context",
|
||||||
|
slug="test-default-context",
|
||||||
|
designation=FlowDesignation.AUTHENTICATION,
|
||||||
|
)
|
||||||
|
FlowStageBinding.objects.create(
|
||||||
|
flow=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
||||||
|
)
|
||||||
|
|
||||||
|
user = User.objects.create(username="test-user")
|
||||||
|
request = self.request_factory.get(
|
||||||
|
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
|
)
|
||||||
|
request.user = user
|
||||||
|
planner = FlowPlanner(flow)
|
||||||
|
planner.plan(request, default_context={PLAN_CONTEXT_PENDING_USER: user})
|
||||||
|
key = cache_key(flow, user)
|
||||||
|
self.assertTrue(cache.get(key) is not None)
|
||||||
|
|
|
@ -34,7 +34,7 @@ class FlowExecutorView(View):
|
||||||
|
|
||||||
def setup(self, request: HttpRequest, flow_slug: str):
|
def setup(self, request: HttpRequest, flow_slug: str):
|
||||||
super().setup(request, flow_slug=flow_slug)
|
super().setup(request, flow_slug=flow_slug)
|
||||||
self.flow = get_object_or_404(Flow, slug=flow_slug)
|
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
|
||||||
|
|
||||||
def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
|
def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
|
||||||
"""When a flow is non-applicable check if user is on the correct domain"""
|
"""When a flow is non-applicable check if user is on the correct domain"""
|
||||||
|
|
|
@ -16,6 +16,8 @@ class IdentificationStageSerializer(ModelSerializer):
|
||||||
"name",
|
"name",
|
||||||
"user_fields",
|
"user_fields",
|
||||||
"template",
|
"template",
|
||||||
|
"enrollment_flow",
|
||||||
|
"recovery_flow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 3.0.6 on 2020-05-30 22:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_flows", "0002_default_flows"),
|
||||||
|
("passbook_stages_identification", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="identificationstage",
|
||||||
|
name="enrollment_flow",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="Optional enrollment flow, which is linked at the bottom of the page.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||||
|
related_name="+",
|
||||||
|
to="passbook_flows.Flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="identificationstage",
|
||||||
|
name="recovery_flow",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="Optional enrollment flow, which is linked at the bottom of the page.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||||
|
related_name="+",
|
||||||
|
to="passbook_flows.Flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -3,7 +3,7 @@ from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from passbook.flows.models import Stage
|
from passbook.flows.models import Flow, Stage
|
||||||
|
|
||||||
|
|
||||||
class UserFields(models.TextChoices):
|
class UserFields(models.TextChoices):
|
||||||
|
@ -29,6 +29,29 @@ class IdentificationStage(Stage):
|
||||||
)
|
)
|
||||||
template = models.TextField(choices=Templates.choices)
|
template = models.TextField(choices=Templates.choices)
|
||||||
|
|
||||||
|
enrollment_flow = models.ForeignKey(
|
||||||
|
Flow,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
default=None,
|
||||||
|
help_text=_(
|
||||||
|
"Optional enrollment flow, which is linked at the bottom of the page."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
recovery_flow = models.ForeignKey(
|
||||||
|
Flow,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
default=None,
|
||||||
|
help_text=_(
|
||||||
|
"Optional enrollment flow, which is linked at the bottom of the page."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
type = "passbook.stages.identification.stage.IdentificationStageView"
|
type = "passbook.stages.identification.stage.IdentificationStageView"
|
||||||
form = "passbook.stages.identification.forms.IdentificationStageForm"
|
form = "passbook.stages.identification.forms.IdentificationStageForm"
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ from django.views.generic import FormView
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Source, User
|
from passbook.core.models import Source, User
|
||||||
from passbook.flows.models import FlowDesignation
|
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from passbook.flows.stage import StageView
|
from passbook.flows.stage import StageView
|
||||||
from passbook.stages.identification.forms import IdentificationForm
|
from passbook.stages.identification.forms import IdentificationForm
|
||||||
|
@ -34,18 +33,17 @@ class IdentificationStageView(FormView, StageView):
|
||||||
return [current_stage.template]
|
return [current_stage.template]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
current_stage: IdentificationStage = self.executor.current_stage
|
||||||
# Check for related enrollment and recovery flow, add URL to view
|
# Check for related enrollment and recovery flow, add URL to view
|
||||||
enrollment_flow = self.executor.flow.related_flow(FlowDesignation.ENROLLMENT)
|
if current_stage.enrollment_flow:
|
||||||
if enrollment_flow:
|
|
||||||
kwargs["enroll_url"] = reverse(
|
kwargs["enroll_url"] = reverse(
|
||||||
"passbook_flows:flow-executor-shell",
|
"passbook_flows:flow-executor-shell",
|
||||||
kwargs={"flow_slug": enrollment_flow.slug},
|
kwargs={"flow_slug": current_stage.enrollment_flow.slug},
|
||||||
)
|
)
|
||||||
recovery_flow = self.executor.flow.related_flow(FlowDesignation.RECOVERY)
|
if current_stage.recovery_flow:
|
||||||
if recovery_flow:
|
|
||||||
kwargs["recovery_url"] = reverse(
|
kwargs["recovery_url"] = reverse(
|
||||||
"passbook_flows:flow-executor-shell",
|
"passbook_flows:flow-executor-shell",
|
||||||
kwargs={"flow_slug": recovery_flow.slug},
|
kwargs={"flow_slug": current_stage.recovery_flow.slug},
|
||||||
)
|
)
|
||||||
kwargs["primary_action"] = _("Log in")
|
kwargs["primary_action"] = _("Log in")
|
||||||
|
|
||||||
|
|
|
@ -85,15 +85,19 @@ class TestIdentificationStage(TestCase):
|
||||||
slug="unique-enrollment-string",
|
slug="unique-enrollment-string",
|
||||||
designation=FlowDesignation.ENROLLMENT,
|
designation=FlowDesignation.ENROLLMENT,
|
||||||
)
|
)
|
||||||
|
self.stage.enrollment_flow = flow
|
||||||
|
self.stage.save()
|
||||||
FlowStageBinding.objects.create(
|
FlowStageBinding.objects.create(
|
||||||
flow=flow, stage=self.stage, order=0,
|
flow=flow, stage=self.stage, order=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
reverse(
|
||||||
|
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn(flow.name, response.rendered_content)
|
self.assertIn(flow.slug, response.rendered_content)
|
||||||
|
|
||||||
def test_recovery_flow(self):
|
def test_recovery_flow(self):
|
||||||
"""Test that recovery flow is linked correctly"""
|
"""Test that recovery flow is linked correctly"""
|
||||||
|
@ -102,12 +106,16 @@ class TestIdentificationStage(TestCase):
|
||||||
slug="unique-recovery-string",
|
slug="unique-recovery-string",
|
||||||
designation=FlowDesignation.RECOVERY,
|
designation=FlowDesignation.RECOVERY,
|
||||||
)
|
)
|
||||||
|
self.stage.recovery_flow = flow
|
||||||
|
self.stage.save()
|
||||||
FlowStageBinding.objects.create(
|
FlowStageBinding.objects.create(
|
||||||
flow=flow, stage=self.stage, order=0,
|
flow=flow, stage=self.stage, order=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
reverse(
|
||||||
|
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn(flow.name, response.rendered_content)
|
self.assertIn(flow.slug, response.rendered_content)
|
||||||
|
|
14
swagger.yaml
14
swagger.yaml
|
@ -5919,6 +5919,20 @@ definitions:
|
||||||
enum:
|
enum:
|
||||||
- stages/identification/login.html
|
- stages/identification/login.html
|
||||||
- stages/identification/recovery.html
|
- stages/identification/recovery.html
|
||||||
|
enrollment_flow:
|
||||||
|
title: Enrollment flow
|
||||||
|
description: Optional enrollment flow, which is linked at the bottom of the
|
||||||
|
page.
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
x-nullable: true
|
||||||
|
recovery_flow:
|
||||||
|
title: Recovery flow
|
||||||
|
description: Optional enrollment flow, which is linked at the bottom of the
|
||||||
|
page.
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
x-nullable: true
|
||||||
InvitationStage:
|
InvitationStage:
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
|
Reference in New Issue