policies: change .form() and .serializer() to properties, add tests

This commit is contained in:
Jens Langhammer 2020-09-29 10:32:41 +02:00
parent 5da4ff4ff1
commit 9724ded194
42 changed files with 188 additions and 7 deletions

View File

@ -64,7 +64,7 @@ class InheritanceUpdateView(UpdateView):
return kwargs
def get_form_class(self):
return self.get_object().form()
return self.get_object().form
def get_object(self, queryset=None):
return (

View File

@ -136,6 +136,7 @@ class Provider(models.Model):
Can return None for providers that are not URL-based"""
return None
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
@ -220,6 +221,7 @@ class Source(PolicyBindingModel):
objects = InheritanceManager()
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
@ -321,6 +323,7 @@ class PropertyMapping(models.Model):
objects = InheritanceManager()
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError

View File

@ -62,6 +62,6 @@ class ServerErrorView(TemplateView):
template_name = "error/generic.html"
# pylint: disable=useless-super-delegation
def dispatch(self, *args, **kwargs):
def dispatch(self, *args, **kwargs): # pragma: no cover
"""Little wrapper so django accepts this function"""
return super().dispatch(*args, **kwargs)

View File

@ -50,6 +50,7 @@ class Stage(SerializerModel):
objects = InheritanceManager()
@property
def type(self) -> Type["StageView"]:
"""Return StageView class that implements logic for this stage"""
# This is a bit of a workaround, since we can't set class methods with setattr
@ -57,6 +58,7 @@ class Stage(SerializerModel):
return getattr(self, "__in_memory_type")
raise NotImplementedError
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError

View File

@ -0,0 +1,31 @@
"""flow model tests"""
from typing import Callable, Type
from django.forms import ModelForm
from django.test import TestCase
from passbook.flows.models import Stage
from passbook.flows.stage import StageView
class TestStageProperties(TestCase):
"""Generic model properties tests"""
def stage_tester_factory(model: Type[Stage]) -> Callable:
"""Test a form"""
def tester(self: TestStageProperties):
model_inst = model()
self.assertTrue(issubclass(model_inst.form, ModelForm))
self.assertTrue(issubclass(model_inst.type, StageView))
return tester
for stage_type in Stage.__subclasses__():
setattr(
TestStageProperties,
f"test_stage_{stage_type.__name__}",
stage_tester_factory(stage_type),
)

View File

@ -96,7 +96,7 @@ class FlowExecutorView(View):
current_stage=self.current_stage,
flow_slug=self.flow.slug,
)
stage_cls = self.current_stage.type()
stage_cls = self.current_stage.type
self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args
self.current_stage_view.kwargs = self.kwargs

30
passbook/lib/tests.py Normal file
View File

@ -0,0 +1,30 @@
"""base model tests"""
from typing import Callable, Type
from django.test import TestCase
from rest_framework.serializers import BaseSerializer
from passbook.flows.models import Stage
from passbook.lib.models import SerializerModel
from passbook.lib.utils.reflection import all_subclasses
class TestModels(TestCase):
"""Generic model properties tests"""
def model_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test a form"""
def tester(self: TestModels):
model_inst = test_model()
try:
self.assertTrue(issubclass(model_inst.serializer, BaseSerializer))
except NotImplementedError:
pass
return tester
for model in all_subclasses(SerializerModel):
setattr(TestModels, f"test_model_{model.__name__}", model_tester_factory(model))

View File

@ -31,6 +31,7 @@ class DummyPolicy(Policy):
return DummyPolicySerializer
@property
def form(self) -> Type[ModelForm]:
from passbook.policies.dummy.forms import DummyPolicyForm

View File

@ -50,7 +50,7 @@ class PolicyEngine:
def __init__(
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
):
if not isinstance(pbm, PolicyBindingModel):
if not isinstance(pbm, PolicyBindingModel): # pragma: no cover
raise ValueError(f"{pbm} is not instance of PolicyBindingModel")
self.__pbm = pbm
self.request = PolicyRequest(user)

View File

@ -28,6 +28,7 @@ class PasswordExpiryPolicy(Policy):
return PasswordExpiryPolicySerializer
@property
def form(self) -> Type[ModelForm]:
from passbook.policies.expiry.forms import PasswordExpiryPolicyForm

View File

@ -22,6 +22,7 @@ class ExpressionPolicy(Policy):
return ExpressionPolicySerializer
@property
def form(self) -> Type[ModelForm]:
from passbook.policies.expression.forms import ExpressionPolicyForm

View File

@ -24,6 +24,7 @@ class GroupMembershipPolicy(Policy):
return GroupMembershipPolicySerializer
@property
def form(self) -> Type[ModelForm]:
from passbook.policies.group_membership.forms import GroupMembershipPolicyForm

View File

@ -34,6 +34,7 @@ class HaveIBeenPwendPolicy(Policy):
return HaveIBeenPwendPolicySerializer
@property
def form(self) -> Type[ModelForm]:
from passbook.policies.hibp.forms import HaveIBeenPwnedPolicyForm

View File

@ -83,14 +83,15 @@ class Policy(SerializerModel, CreatedUpdatedModel):
objects = InheritanceAutoManager()
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
def __str__(self):
return f"Policy {self.name}"
return f"{self.__class__.__name__} {self.name}"
def passes(self, request: PolicyRequest) -> PolicyResult:
def passes(self, request: PolicyRequest) -> PolicyResult: # pragma: no cover
"""Check if user instance passes this policy"""
raise PolicyException()

View File

@ -37,6 +37,7 @@ class PasswordPolicy(Policy):
return PasswordPolicySerializer
@property
def form(self) -> Type[ModelForm]:
from passbook.policies.password.forms import PasswordPolicyForm

View File

@ -29,6 +29,7 @@ class ReputationPolicy(Policy):
return ReputationPolicySerializer
@property
def form(self) -> Type[ModelForm]:
from passbook.policies.reputation.forms import ReputationPolicyForm

View File

View File

@ -0,0 +1,30 @@
"""flow model tests"""
from typing import Callable, Type
from django.forms import ModelForm
from django.test import TestCase
from passbook.lib.utils.reflection import all_subclasses
from passbook.policies.models import Policy
class TestPolicyProperties(TestCase):
"""Generic model properties tests"""
def policy_tester_factory(model: Type[Policy]) -> Callable:
"""Test a form"""
def tester(self: TestPolicyProperties):
model_inst = model()
self.assertTrue(issubclass(model_inst.form, ModelForm))
return tester
for policy_type in all_subclasses(Policy):
setattr(
TestPolicyProperties,
f"test_policy_{policy_type.__name__}",
policy_tester_factory(policy_type),
)

View File

@ -102,6 +102,7 @@ class ScopeMapping(PropertyMapping):
),
)
@property
def form(self) -> Type[ModelForm]:
from passbook.providers.oauth2.forms import ScopeMappingForm
@ -264,6 +265,7 @@ class OAuth2Provider(Provider):
launch_url = urlparse(main_url)
return main_url.replace(launch_url.path, "")
@property
def form(self) -> Type[ModelForm]:
from passbook.providers.oauth2.forms import OAuth2ProviderForm

View File

@ -67,6 +67,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
cookie_secret = models.TextField(default=get_cookie_secret)
@property
def form(self) -> Type[ModelForm]:
from passbook.providers.proxy.forms import ProxyProviderForm

View File

@ -0,0 +1,32 @@
"""Test Controllers"""
import yaml
from django.test import TestCase
from passbook.flows.models import Flow
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
from passbook.providers.proxy.controllers.kubernetes import KubernetesController
from passbook.providers.proxy.models import ProxyProvider
class TestControllers(TestCase):
"""Test Controllers"""
def test_kubernetes_controller(self):
"""Test Kubernetes Controller"""
provider: ProxyProvider = ProxyProvider.objects.create(
name="test",
internal_host="http://localhost",
external_host="http://localhost",
authorization_flow=Flow.objects.first(),
)
outpost: Outpost = Outpost.objects.create(
name="test",
type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.CUSTOM,
)
outpost.providers.add(provider)
outpost.save()
controller = KubernetesController(outpost.pk)
manifest = controller.get_static_deployment()
self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 3)

View File

@ -109,6 +109,7 @@ class SAMLProvider(Provider):
launch_url = urlparse(self.acs_url)
return self.acs_url.replace(launch_url.path, "")
@property
def form(self) -> Type[ModelForm]:
from passbook.providers.saml.forms import SAMLProviderForm
@ -154,6 +155,7 @@ class SAMLPropertyMapping(PropertyMapping):
saml_name = models.TextField(verbose_name="SAML Name")
friendly_name = models.TextField(default=None, blank=True, null=True)
@property
def form(self) -> Type[ModelForm]:
from passbook.providers.saml.forms import SAMLPropertyMappingForm

View File

@ -67,6 +67,7 @@ class LDAPSource(Source):
Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT
)
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.ldap.forms import LDAPSourceForm
@ -116,6 +117,7 @@ class LDAPPropertyMapping(PropertyMapping):
object_field = models.TextField()
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.ldap.forms import LDAPPropertyMappingForm

View File

@ -40,6 +40,7 @@ class OAuthSource(Source):
consumer_key = models.TextField()
consumer_secret = models.TextField()
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.oauth.forms import OAuthSourceForm
@ -83,6 +84,7 @@ class OAuthSource(Source):
class GitHubOAuthSource(OAuthSource):
"""Social Login using GitHub.com or a GitHub-Enterprise Instance."""
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.oauth.forms import GitHubOAuthSourceForm
@ -98,6 +100,7 @@ class GitHubOAuthSource(OAuthSource):
class TwitterOAuthSource(OAuthSource):
"""Social Login using Twitter.com"""
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.oauth.forms import TwitterOAuthSourceForm
@ -113,6 +116,7 @@ class TwitterOAuthSource(OAuthSource):
class FacebookOAuthSource(OAuthSource):
"""Social Login using Facebook.com."""
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.oauth.forms import FacebookOAuthSourceForm
@ -128,6 +132,7 @@ class FacebookOAuthSource(OAuthSource):
class DiscordOAuthSource(OAuthSource):
"""Social Login using Discord."""
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.oauth.forms import DiscordOAuthSourceForm
@ -143,6 +148,7 @@ class DiscordOAuthSource(OAuthSource):
class GoogleOAuthSource(OAuthSource):
"""Social Login using Google or Gsuite."""
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.oauth.forms import GoogleOAuthSourceForm
@ -158,6 +164,7 @@ class GoogleOAuthSource(OAuthSource):
class AzureADOAuthSource(OAuthSource):
"""Social Login using Azure AD."""
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.oauth.forms import AzureADOAuthSourceForm
@ -173,6 +180,7 @@ class AzureADOAuthSource(OAuthSource):
class OpenIDOAuthSource(OAuthSource):
"""Login using a Generic OpenID-Connect compliant provider."""
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.oauth.forms import OAuthSourceForm

View File

@ -103,6 +103,7 @@ class SAMLSource(Source):
on_delete=models.PROTECT,
)
@property
def form(self) -> Type[ModelForm]:
from passbook.sources.saml.forms import SAMLSourceForm

View File

@ -30,11 +30,13 @@ class CaptchaStage(Stage):
return CaptchaStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.captcha.stage import CaptchaStageView
return CaptchaStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.captcha.forms import CaptchaStageForm

View File

@ -44,11 +44,13 @@ class ConsentStage(Stage):
return ConsentStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.consent.stage import ConsentStageView
return ConsentStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.consent.forms import ConsentStageForm

View File

@ -20,11 +20,13 @@ class DummyStage(Stage):
return DummyStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.dummy.stage import DummyStageView
return DummyStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.dummy.forms import DummyStageForm

View File

@ -51,11 +51,13 @@ class EmailStage(Stage):
return EmailStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.email.stage import EmailStageView
return EmailStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.email.forms import EmailStageForm

View File

@ -63,11 +63,13 @@ class IdentificationStage(Stage):
return IdentificationStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.identification.stage import IdentificationStageView
return IdentificationStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.identification.forms import IdentificationStageForm

View File

@ -33,11 +33,13 @@ class InvitationStage(Stage):
return InvitationStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.invitation.stage import InvitationStageView
return InvitationStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.invitation.forms import InvitationStageForm

View File

@ -23,11 +23,13 @@ class OTPStaticStage(ConfigurableStage, Stage):
return OTPStaticStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.otp_static.stage import OTPStaticStageView
return OTPStaticStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.otp_static.forms import OTPStaticStageForm

View File

@ -30,11 +30,13 @@ class OTPTimeStage(ConfigurableStage, Stage):
return OTPTimeStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.otp_time.stage import OTPTimeStageView
return OTPTimeStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.otp_time.forms import OTPTimeStageForm

View File

@ -23,11 +23,13 @@ class OTPValidateStage(Stage):
return OTPValidateStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.otp_validate.stage import OTPValidateStageView
return OTPValidateStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.otp_validate.forms import OTPValidateStageForm

View File

@ -38,11 +38,13 @@ class PasswordStage(ConfigurableStage, Stage):
return PasswordStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.password.stage import PasswordStageView
return PasswordStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.password.forms import PasswordStageForm

View File

@ -145,11 +145,13 @@ class PromptStage(Stage):
return PromptStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.prompt.stage import PromptStageView
return PromptStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.prompt.forms import PromptStageForm

View File

@ -19,11 +19,13 @@ class UserDeleteStage(Stage):
return UserDeleteStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.user_delete.stage import UserDeleteStageView
return UserDeleteStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.user_delete.forms import UserDeleteStageForm

View File

@ -27,11 +27,13 @@ class UserLoginStage(Stage):
return UserLoginStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.user_login.stage import UserLoginStageView
return UserLoginStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.user_login.forms import UserLoginStageForm

View File

@ -18,11 +18,13 @@ class UserLogoutStage(Stage):
return UserLogoutStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.user_logout.stage import UserLogoutStageView
return UserLogoutStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.user_logout.forms import UserLogoutStageForm

View File

@ -19,11 +19,13 @@ class UserWriteStage(Stage):
return UserWriteStageSerializer
@property
def type(self) -> Type[View]:
from passbook.stages.user_write.stage import UserWriteStageView
return UserWriteStageView
@property
def form(self) -> Type[ModelForm]:
from passbook.stages.user_write.forms import UserWriteStageForm

View File

@ -1,6 +1,5 @@
[pytest]
DJANGO_SETTINGS_MODULE = passbook.root.settings
# -- recommended but optional:
python_files = tests.py test_*.py *_tests.py
junit_family = xunit2
addopts = -p no:celery --junitxml=unittest.xml