From 6a4086c4901f30cdc30c2cc15881404079ea7727 Mon Sep 17 00:00:00 2001 From: Jens L Date: Thu, 18 Jun 2020 22:43:51 +0200 Subject: [PATCH] =?UTF-8?q?flows:=20introduce=20FlowPlan=20markers,=20whic?= =?UTF-8?q?h=20indicate=20when=20a=20stage=20needs=20=E2=80=A6=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * flows: introduce FlowPlan markers, which indicate when a stage needs re-evaluation Implement re_evaluate_policies add unittests for several different scenarios closes #78 * flows: move markers to separate files, cleanup formatting * flows: fix self.next is not callable --- passbook/flows/markers.py | 49 +++++ passbook/flows/planner.py | 50 +++++- passbook/flows/tests/test_planner.py | 78 +++++++- passbook/flows/tests/test_views.py | 207 +++++++++++++++++++++- passbook/flows/tests/test_views_helper.py | 2 +- passbook/flows/views.py | 7 +- passbook/stages/captcha/tests.py | 5 +- passbook/stages/consent/tests.py | 5 +- passbook/stages/dummy/stage.py | 7 + passbook/stages/email/tests.py | 17 +- passbook/stages/invitation/tests.py | 13 +- passbook/stages/password/tests.py | 21 ++- passbook/stages/prompt/tests.py | 17 +- passbook/stages/user_delete/tests.py | 13 +- passbook/stages/user_login/tests.py | 13 +- passbook/stages/user_logout/tests.py | 5 +- passbook/stages/user_write/tests.py | 13 +- 17 files changed, 482 insertions(+), 40 deletions(-) create mode 100644 passbook/flows/markers.py diff --git a/passbook/flows/markers.py b/passbook/flows/markers.py new file mode 100644 index 000000000..f4fd97178 --- /dev/null +++ b/passbook/flows/markers.py @@ -0,0 +1,49 @@ +"""Stage Markers""" +from dataclasses import dataclass +from typing import TYPE_CHECKING, Optional + +from structlog import get_logger + +from passbook.core.models import User +from passbook.flows.models import Stage +from passbook.policies.engine import PolicyEngine +from passbook.policies.models import PolicyBinding + +if TYPE_CHECKING: + from passbook.flows.planner import FlowPlan + +LOGGER = get_logger() + + +@dataclass +class StageMarker: + """Base stage marker class, no extra attributes, and has no special handler.""" + + # pylint: disable=unused-argument + def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]: + """Process callback for this marker. This should be overridden by sub-classes. + If a stage should be removed, return None.""" + return stage + + +@dataclass +class ReevaluateMarker(StageMarker): + """Reevaluate Marker, forces stage's policies to be evaluated again.""" + + binding: PolicyBinding + user: User + + def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]: + """Re-evaluate policies bound to stage, and if they fail, remove from plan""" + engine = PolicyEngine(self.binding, self.user) + engine.request.context = plan.context + engine.build() + result = engine.result + if result.passing: + return stage + LOGGER.warning( + "f(plan_inst)[re-eval marker]: stage failed re-evaluation", + stage=stage, + messages=result.messages, + ) + return None diff --git a/passbook/flows/planner.py b/passbook/flows/planner.py index ca8a81c5e..86acdb630 100644 --- a/passbook/flows/planner.py +++ b/passbook/flows/planner.py @@ -9,7 +9,8 @@ from structlog import get_logger from passbook.core.models import User from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException -from passbook.flows.models import Flow, Stage +from passbook.flows.markers import ReevaluateMarker, StageMarker +from passbook.flows.models import Flow, FlowStageBinding, Stage from passbook.policies.engine import PolicyEngine LOGGER = get_logger() @@ -33,12 +34,39 @@ class FlowPlan: of all Stages that should be run.""" flow_pk: str + stages: List[Stage] = field(default_factory=list) context: Dict[str, Any] = field(default_factory=dict) + markers: List[StageMarker] = field(default_factory=list) - def next(self) -> Stage: + def next(self) -> Optional[Stage]: """Return next pending stage from the bottom of the list""" - return self.stages[0] + if not self.has_stages: + return None + stage = self.stages[0] + marker = self.markers[0] + + LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker) + marked_stage = marker.process(self, stage) + if not marked_stage: + LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage) + self.stages.remove(stage) + self.markers.remove(marker) + if not self.has_stages: + return None + # pylint: disable=not-callable + return self.next() + return marked_stage + + def pop(self): + """Pop next pending stage from bottom of list""" + self.markers.pop(0) + self.stages.pop(0) + + @property + def has_stages(self) -> bool: + """Check if there are any stages left in this plan""" + return len(self.markers) + len(self.stages) > 0 class FlowPlanner: @@ -100,7 +128,8 @@ class FlowPlanner: request: HttpRequest, default_context: Optional[Dict[str, Any]], ) -> FlowPlan: - """Actually build flow plan""" + """Build flow plan by checking each stage in their respective + order and checking the applied policies""" start_time = time() plan = FlowPlan(flow_pk=self.flow.pk.hex) if default_context: @@ -111,13 +140,24 @@ class FlowPlanner: .select_subclasses() .select_related() ): - binding = stage.flowstagebinding_set.get(flow__pk=self.flow.pk) + binding: FlowStageBinding = stage.flowstagebinding_set.get( + flow__pk=self.flow.pk + ) engine = PolicyEngine(binding, user, request) engine.request.context = plan.context engine.build() if engine.passing: LOGGER.debug("f(plan): Stage passing", stage=stage, flow=self.flow) plan.stages.append(stage) + marker = StageMarker() + if binding.re_evaluate_policies: + LOGGER.debug( + "f(plan): Stage has re-evaluate marker", + stage=stage, + flow=self.flow, + ) + marker = ReevaluateMarker(binding=binding, user=user) + plan.markers.append(marker) end_time = time() LOGGER.debug( "f(plan): Finished building", diff --git a/passbook/flows/tests/test_planner.py b/passbook/flows/tests/test_planner.py index af6ae98fa..8c65e6636 100644 --- a/passbook/flows/tests/test_planner.py +++ b/passbook/flows/tests/test_planner.py @@ -1,6 +1,7 @@ """flow planner tests""" from unittest.mock import MagicMock, PropertyMock, patch +from django.contrib.sessions.middleware import SessionMiddleware from django.core.cache import cache from django.shortcuts import reverse from django.test import RequestFactory, TestCase @@ -8,14 +9,19 @@ from guardian.shortcuts import get_anonymous_user from passbook.core.models import User from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException +from passbook.flows.markers import ReevaluateMarker, StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key +from passbook.policies.dummy.models import DummyPolicy +from passbook.policies.models import PolicyBinding from passbook.policies.types import PolicyResult from passbook.stages.dummy.models import DummyStage -POLICY_RESULT_MOCK = PropertyMock(return_value=PolicyResult(False)) +POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False)) TIME_NOW_MOCK = MagicMock(return_value=3) +POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True)) + class TestFlowPlanner(TestCase): """Test planner logic""" @@ -40,7 +46,7 @@ class TestFlowPlanner(TestCase): planner.plan(request) @patch( - "passbook.policies.engine.PolicyEngine.result", POLICY_RESULT_MOCK, + "passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE, ) def test_non_applicable_plan(self): """Test that empty plan raises exception""" @@ -103,3 +109,71 @@ class TestFlowPlanner(TestCase): planner.plan(request, default_context={PLAN_CONTEXT_PENDING_USER: user}) key = cache_key(flow, user) self.assertTrue(cache.get(key) is not None) + + def test_planner_marker_reevaluate(self): + """Test that the planner creates the proper marker""" + 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="dummy1"), + order=0, + re_evaluate_policies=True, + ) + + request = self.request_factory.get( + reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + ) + request.user = get_anonymous_user() + + planner = FlowPlanner(flow) + plan = planner.plan(request) + + self.assertIsInstance(plan.markers[0], ReevaluateMarker) + + def test_planner_reevaluate_actual(self): + """Test planner with re-evaluate""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) + + binding = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + ) + binding2 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy2"), + order=1, + re_evaluate_policies=True, + ) + + PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) + + request = self.request_factory.get( + reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + ) + request.user = get_anonymous_user() + + middleware = SessionMiddleware() + middleware.process_request(request) + request.session.save() + + # Here we patch the dummy policy to evaluate to true so the stage is included + with patch( + "passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE + ): + planner = FlowPlanner(flow) + plan = planner.plan(request) + + self.assertEqual(plan.stages[0], binding.stage) + self.assertEqual(plan.stages[1], binding2.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], ReevaluateMarker) diff --git a/passbook/flows/tests/test_views.py b/passbook/flows/tests/test_views.py index cacbe2004..f68173582 100644 --- a/passbook/flows/tests/test_views.py +++ b/passbook/flows/tests/test_views.py @@ -3,16 +3,21 @@ from unittest.mock import MagicMock, PropertyMock, patch from django.shortcuts import reverse from django.test import Client, TestCase +from django.utils.encoding import force_text from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException +from passbook.flows.markers import ReevaluateMarker, StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN from passbook.lib.config import CONFIG +from passbook.policies.dummy.models import DummyPolicy +from passbook.policies.models import PolicyBinding from passbook.policies.types import PolicyResult from passbook.stages.dummy.models import DummyStage -POLICY_RESULT_MOCK = PropertyMock(return_value=PolicyResult(False)) +POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False)) +POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True)) class TestFlowExecutor(TestCase): @@ -29,7 +34,9 @@ class TestFlowExecutor(TestCase): designation=FlowDesignation.AUTHENTICATION, ) stage = DummyStage.objects.create(name="dummy") - plan = FlowPlan(flow_pk=flow.pk.hex + "a", stages=[stage]) + plan = FlowPlan( + flow_pk=flow.pk.hex + "a", stages=[stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -45,7 +52,7 @@ class TestFlowExecutor(TestCase): self.assertEqual(cancel_mock.call_count, 1) @patch( - "passbook.policies.engine.PolicyEngine.result", POLICY_RESULT_MOCK, + "passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE, ) def test_invalid_non_applicable_flow(self): """Tests that a non-applicable flow returns the correct error message""" @@ -125,3 +132,197 @@ class TestFlowExecutor(TestCase): session = self.client.session plan: FlowPlan = session[SESSION_KEY_PLAN] self.assertEqual(len(plan.stages), 1) + + def test_reevaluate_remove_last(self): + """Test planner with re-evaluate (last stage is removed)""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) + + binding = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + ) + binding2 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy2"), + order=1, + re_evaluate_policies=True, + ) + + PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) + + # Here we patch the dummy policy to evaluate to true so the stage is included + with patch( + "passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE + ): + + exec_url = reverse( + "passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug} + ) + # First request, run the planner + response = self.client.get(exec_url) + self.assertEqual(response.status_code, 200) + + plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] + + self.assertEqual(plan.stages[0], binding.stage) + self.assertEqual(plan.stages[1], binding2.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], ReevaluateMarker) + + # Second request, this passes the first dummy stage + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 302) + + # third request, this should trigger the re-evaluate + # We do this request without the patch, so the policy results in false + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("passbook_core:overview")) + + def test_reevaluate_remove_middle(self): + """Test planner with re-evaluate (middle stage is removed)""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) + + binding = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + ) + binding2 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy2"), + order=1, + re_evaluate_policies=True, + ) + binding3 = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy3"), order=2 + ) + + PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) + + # Here we patch the dummy policy to evaluate to true so the stage is included + with patch( + "passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE + ): + + exec_url = reverse( + "passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug} + ) + # First request, run the planner + response = self.client.get(exec_url) + + self.assertEqual(response.status_code, 200) + plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] + + self.assertEqual(plan.stages[0], binding.stage) + self.assertEqual(plan.stages[1], binding2.stage) + self.assertEqual(plan.stages[2], binding3.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], ReevaluateMarker) + self.assertIsInstance(plan.markers[2], StageMarker) + + # Second request, this passes the first dummy stage + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 302) + + plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] + + self.assertEqual(plan.stages[0], binding2.stage) + self.assertEqual(plan.stages[1], binding3.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], StageMarker) + + # third request, this should trigger the re-evaluate + # We do this request without the patch, so the policy results in false + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 200) + self.assertJSONEqual( + force_text(response.content), + {"type": "redirect", "to": reverse("passbook_core:overview")}, + ) + + def test_reevaluate_remove_consecutive(self): + """Test planner with re-evaluate (consecutive stages are removed)""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) + + binding = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + ) + binding2 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy2"), + order=1, + re_evaluate_policies=True, + ) + binding3 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy3"), + order=2, + re_evaluate_policies=True, + ) + binding4 = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy4"), order=2 + ) + + PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) + PolicyBinding.objects.create(policy=false_policy, target=binding3, order=0) + + # Here we patch the dummy policy to evaluate to true so the stage is included + with patch( + "passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE + ): + + exec_url = reverse( + "passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug} + ) + # First request, run the planner + response = self.client.get(exec_url) + self.assertEqual(response.status_code, 200) + self.assertIn("dummy1", force_text(response.content)) + + plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] + + self.assertEqual(plan.stages[0], binding.stage) + self.assertEqual(plan.stages[1], binding2.stage) + self.assertEqual(plan.stages[2], binding3.stage) + self.assertEqual(plan.stages[3], binding4.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], ReevaluateMarker) + self.assertIsInstance(plan.markers[2], ReevaluateMarker) + self.assertIsInstance(plan.markers[3], StageMarker) + + # Second request, this passes the first dummy stage + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 302) + + # third request, this should trigger the re-evaluate + # A get request will evaluate the policies and this will return stage 4 + # but it won't save it, hence we cant' check the plan + response = self.client.get(exec_url) + self.assertEqual(response.status_code, 200) + self.assertIn("dummy4", force_text(response.content)) + + # fourth request, this confirms the last stage (dummy4) + # We do this request without the patch, so the policy results in false + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 200) + self.assertJSONEqual( + force_text(response.content), + {"type": "redirect", "to": reverse("passbook_core:overview")}, + ) diff --git a/passbook/flows/tests/test_views_helper.py b/passbook/flows/tests/test_views_helper.py index 0f59e4b7e..435146f5b 100644 --- a/passbook/flows/tests/test_views_helper.py +++ b/passbook/flows/tests/test_views_helper.py @@ -26,7 +26,7 @@ class TestHelperView(TestCase): def test_default_view_invalid_plan(self): """Test that ToDefaultFlow returns the expected URL (with an invalid plan)""" flow = Flow.objects.filter(designation=FlowDesignation.INVALIDATION,).first() - plan = FlowPlan(flow_pk=flow.pk.hex + "aa", stages=[]) + plan = FlowPlan(flow_pk=flow.pk.hex + "aa") session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/flows/views.py b/passbook/flows/views.py index 3613a7017..65abf7ff3 100644 --- a/passbook/flows/views.py +++ b/passbook/flows/views.py @@ -86,6 +86,9 @@ class FlowExecutorView(View): current_stage=self.current_stage, flow_slug=self.flow.slug, ) + if not self.current_stage: + LOGGER.debug("f(exec): no more stages, flow is done.") + return self._flow_done() stage_cls = path_to_class(self.current_stage.type) self.current_stage_view = stage_cls(self) self.current_stage_view.args = self.args @@ -98,6 +101,7 @@ class FlowExecutorView(View): LOGGER.debug( "f(exec): Passing GET", view_class=class_to_path(self.current_stage_view.__class__), + stage=self.current_stage, flow_slug=self.flow.slug, ) stage_response = self.current_stage_view.get(request, *args, **kwargs) @@ -108,6 +112,7 @@ class FlowExecutorView(View): LOGGER.debug( "f(exec): Passing POST", view_class=class_to_path(self.current_stage_view.__class__), + stage=self.current_stage, flow_slug=self.flow.slug, ) stage_response = self.current_stage_view.post(request, *args, **kwargs) @@ -133,7 +138,7 @@ class FlowExecutorView(View): stage_class=class_to_path(self.current_stage_view.__class__), flow_slug=self.flow.slug, ) - self.plan.stages.pop(0) + self.plan.pop() self.request.session[SESSION_KEY_PLAN] = self.plan if self.plan.stages: LOGGER.debug( diff --git a/passbook/stages/captcha/tests.py b/passbook/stages/captcha/tests.py index f5818deb3..c5c8f5850 100644 --- a/passbook/stages/captcha/tests.py +++ b/passbook/stages/captcha/tests.py @@ -5,6 +5,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -35,7 +36,9 @@ class TestCaptchaStage(TestCase): def test_valid(self): """Test valid captcha""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/stages/consent/tests.py b/passbook/stages/consent/tests.py index 4d642d495..e7f0f2534 100644 --- a/passbook/stages/consent/tests.py +++ b/passbook/stages/consent/tests.py @@ -4,6 +4,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -30,7 +31,9 @@ class TestConsentStage(TestCase): def test_valid(self): """Test valid consent""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/stages/dummy/stage.py b/passbook/stages/dummy/stage.py index 43a3edcdf..07f3dacff 100644 --- a/passbook/stages/dummy/stage.py +++ b/passbook/stages/dummy/stage.py @@ -1,4 +1,6 @@ """passbook multi-stage authentication engine""" +from typing import Any, Dict + from django.http import HttpRequest from passbook.flows.stage import StageView @@ -10,3 +12,8 @@ class DummyStage(StageView): def post(self, request: HttpRequest): """Just redirect to next stage""" return self.executor.stage_ok() + + def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]: + kwargs = super().get_context_data(**kwargs) + kwargs["title"] = self.executor.current_stage.name + return kwargs diff --git a/passbook/stages/email/tests.py b/passbook/stages/email/tests.py index c65ebff09..43d7e2e7c 100644 --- a/passbook/stages/email/tests.py +++ b/passbook/stages/email/tests.py @@ -7,6 +7,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import Token, User +from passbook.flows.markers import StageMarker 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 @@ -34,7 +35,9 @@ class TestEmailStage(TestCase): def test_rendering(self): """Test with pending user""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -48,7 +51,9 @@ class TestEmailStage(TestCase): def test_without_user(self): """Test without pending user""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -61,7 +66,9 @@ class TestEmailStage(TestCase): def test_pending_user(self): """Test with pending user""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -82,7 +89,9 @@ class TestEmailStage(TestCase): """Test with token""" # Make sure token exists self.test_pending_user() - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/stages/invitation/tests.py b/passbook/stages/invitation/tests.py index 4e7dbb7db..f47ddc766 100644 --- a/passbook/stages/invitation/tests.py +++ b/passbook/stages/invitation/tests.py @@ -7,6 +7,7 @@ from django.utils.encoding import force_text from guardian.shortcuts import get_anonymous_user from passbook.core.models import User +from passbook.flows.markers import StageMarker 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 @@ -39,7 +40,9 @@ class TestUserLoginStage(TestCase): def test_without_invitation_fail(self): """Test without any invitation, continue_flow_without_invitation not set.""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND @@ -64,7 +67,9 @@ class TestUserLoginStage(TestCase): """Test without any invitation, continue_flow_without_invitation is set.""" self.stage.continue_flow_without_invitation = True self.stage.save() - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND @@ -90,7 +95,9 @@ class TestUserLoginStage(TestCase): def test_with_invitation(self): """Test with invitation, check data in session""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND diff --git a/passbook/stages/password/tests.py b/passbook/stages/password/tests.py index 71364abc1..3dbb6f9ec 100644 --- a/passbook/stages/password/tests.py +++ b/passbook/stages/password/tests.py @@ -9,6 +9,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker 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 @@ -43,7 +44,9 @@ class TestPasswordStage(TestCase): def test_without_user(self): """Test without user""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -68,7 +71,9 @@ class TestPasswordStage(TestCase): designation=FlowDesignation.RECOVERY, slug="qewrqerqr" ) - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -83,7 +88,9 @@ class TestPasswordStage(TestCase): def test_valid_password(self): """Test with a valid pending user and valid password""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -105,7 +112,9 @@ class TestPasswordStage(TestCase): def test_invalid_password(self): """Test with a valid pending user and invalid password""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -127,7 +136,9 @@ class TestPasswordStage(TestCase): def test_permission_denied(self): """Test with a valid pending user and valid password. Backend is patched to return PermissionError""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan diff --git a/passbook/stages/prompt/tests.py b/passbook/stages/prompt/tests.py index df8ad88a3..b5893e8d7 100644 --- a/passbook/stages/prompt/tests.py +++ b/passbook/stages/prompt/tests.py @@ -6,6 +6,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -96,7 +97,9 @@ class TestPromptStage(TestCase): def test_render(self): """Test render of form, check if all prompts are rendered correctly""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -114,7 +117,9 @@ class TestPromptStage(TestCase): def test_valid_form_with_policy(self) -> PromptForm: """Test form validation""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) expr = "return request.context['password_prompt'] == request.context['password2_prompt']" expr_policy = ExpressionPolicy.objects.create( name="validate-form", expression=expr @@ -126,7 +131,9 @@ class TestPromptStage(TestCase): def test_invalid_form(self) -> PromptForm: """Test form validation""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) expr = "False" expr_policy = ExpressionPolicy.objects.create( name="validate-form", expression=expr @@ -138,7 +145,9 @@ class TestPromptStage(TestCase): def test_valid_form_request(self): """Test a request with valid form data""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/stages/user_delete/tests.py b/passbook/stages/user_delete/tests.py index 9d09ddd67..26376c683 100644 --- a/passbook/stages/user_delete/tests.py +++ b/passbook/stages/user_delete/tests.py @@ -4,6 +4,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker 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 @@ -29,7 +30,9 @@ class TestUserDeleteStage(TestCase): def test_no_user(self): """Test without user set""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -47,7 +50,9 @@ class TestUserDeleteStage(TestCase): def test_user_delete_get(self): """Test Form render""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -62,7 +67,9 @@ class TestUserDeleteStage(TestCase): def test_user_delete_post(self): """Test User delete (actual)""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan diff --git a/passbook/stages/user_login/tests.py b/passbook/stages/user_login/tests.py index 990550d94..4509c7b50 100644 --- a/passbook/stages/user_login/tests.py +++ b/passbook/stages/user_login/tests.py @@ -4,6 +4,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker 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 @@ -30,7 +31,9 @@ class TestUserLoginStage(TestCase): def test_valid_password(self): """Test with a valid pending user and backend""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND @@ -53,7 +56,9 @@ class TestUserLoginStage(TestCase): def test_without_user(self): """Test a plan without any pending user, resulting in a denied""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -72,7 +77,9 @@ class TestUserLoginStage(TestCase): def test_without_backend(self): """Test a plan with pending user, without backend, resulting in a denied""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan diff --git a/passbook/stages/user_logout/tests.py b/passbook/stages/user_logout/tests.py index ee526a581..7263ccee2 100644 --- a/passbook/stages/user_logout/tests.py +++ b/passbook/stages/user_logout/tests.py @@ -4,6 +4,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker 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 @@ -30,7 +31,9 @@ class TestUserLogoutStage(TestCase): def test_valid_password(self): """Test with a valid pending user and backend""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND diff --git a/passbook/stages/user_write/tests.py b/passbook/stages/user_write/tests.py index 8aefbd121..d36ce0c06 100644 --- a/passbook/stages/user_write/tests.py +++ b/passbook/stages/user_write/tests.py @@ -7,6 +7,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker 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 @@ -37,7 +38,9 @@ class TestUserWriteStage(TestCase): for _ in range(8) ) - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PROMPT] = { "username": "test-user", "name": "name", @@ -71,7 +74,9 @@ class TestUserWriteStage(TestCase): SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(8) ) - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create( username="unittest", email="test@beryju.org" ) @@ -104,7 +109,9 @@ class TestUserWriteStage(TestCase): def test_without_data(self): """Test without data results in error""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save()