Skip to content

Commit

Permalink
fix: new permissions and fix test cases (#61)
Browse files Browse the repository at this point in the history
* hi florian

* import allow none

* remove spaces

* feedback
  • Loading branch information
SKairinos committed Jan 24, 2024
1 parent b10fb82 commit d6c6a7e
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 12 deletions.
51 changes: 51 additions & 0 deletions .vscode/workspace.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"module.docstring": {
"prefix": [
"module.docstring",
"\"\"\"",
"'''"
],
"scope": "python",
"body": [
"\"\"\"",
"© Ocado Group",
"Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET)."
"",
"${1:__description__}",
"\"\"\""
]
},
"module.doccomment": {
"prefix": [
"module.doccomment",
"/"
],
"scope": "javascript,typescript,javascriptreact,typescriptreact",
"body": [
"/**",
" * © Ocado Group",
" * Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET)."
" *",
" * ${1:__description__}",
" */"
]
},
"pylint.disable-next": {
"prefix": [
"# pylint"
],
"scope": "python",
"body": [
"# pylint: disable-next=${1:__code_name__}"
]
},
"mypy.ignore": {
"prefix": [
"# type"
],
"scope": "python",
"body": [
"# type: ignore[${1:__code_name__}]"
]
}
}
8 changes: 8 additions & 0 deletions codeforlife/permissions/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
"""
© Ocado Group
Created on 23/01/2024 at 16:38:07(+00:00).
Reusable DRF permissions.
"""

from .allow_none import AllowNone
from .is_cron_request_from_google import IsCronRequestFromGoogle
18 changes: 18 additions & 0 deletions codeforlife/permissions/allow_none.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
© Ocado Group
Created on 23/01/2024 at 14:46:23(+00:00).
"""

from rest_framework.permissions import BasePermission


class AllowNone(BasePermission):
"""
Blocks all incoming requests.
This is the opposite of DRF's AllowAny permission:
https://www.django-rest-framework.org/api-guide/permissions/#allowany
"""

def has_permission(self, request, view):
return False
9 changes: 6 additions & 3 deletions codeforlife/permissions/is_cron_request_from_google.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""
© Ocado Group
Created on 23/01/2024 at 14:45:07(+00:00).
"""

from django.conf import settings
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
from rest_framework.views import View


class IsCronRequestFromGoogle(BasePermission):
Expand All @@ -11,7 +14,7 @@ class IsCronRequestFromGoogle(BasePermission):
https://cloud.google.com/appengine/docs/flexible/scheduling-jobs-with-cron-yaml#securing_urls_for_cron
"""

def has_permission(self, request: Request, view: View):
def has_permission(self, request, view):
return (
settings.DEBUG
or request.META.get("HTTP_X_APPENGINE_CRON") == "true"
Expand Down
4 changes: 4 additions & 0 deletions codeforlife/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ class ModelSerializer(_ModelSerializer[AnyModel], t.Generic[AnyModel]):
# pylint: disable-next=useless-parent-delegation
def update(self, instance, validated_data: t.Dict[str, t.Any]):
return super().update(instance, validated_data)

# pylint: disable-next=useless-parent-delegation
def create(self, validated_data: t.Dict[str, t.Any]):
return super().create(validated_data)
35 changes: 26 additions & 9 deletions codeforlife/tests/model_view_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
from django.utils import timezone
from django.utils.http import urlencode
from pyotp import TOTP
from rest_framework import status
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.test import APIClient, APITestCase
from rest_framework.test import APIClient, APIRequestFactory, APITestCase
from rest_framework.viewsets import ModelViewSet

from ..user.models import AuthFactor, User
Expand All @@ -36,7 +37,12 @@ class ModelViewSetClient(
responses.
"""

Data = t.Dict[str, t.Any]
def __init__(self, enforce_csrf_checks: bool = False, **defaults):
super().__init__(enforce_csrf_checks, **defaults)
self.request_factory = APIRequestFactory(
enforce_csrf_checks,
**defaults,
)

_test_case: "ModelViewSetTestCase[AnyModelViewSet, AnyModelSerializer, AnyModel]"

Expand All @@ -61,6 +67,7 @@ def _model_view_set_class(self):
# pylint: disable-next=no-member
return self._test_case.get_model_view_set_class()

Data = t.Dict[str, t.Any]
StatusCodeAssertion = t.Optional[t.Union[int, t.Callable[[int], bool]]]
ListFilters = t.Optional[t.Dict[str, str]]

Expand Down Expand Up @@ -204,7 +211,7 @@ def generic(
def create(
self,
data: Data,
status_code_assertion: StatusCodeAssertion = None,
status_code_assertion: StatusCodeAssertion = status.HTTP_201_CREATED,
**kwargs,
):
"""Create a model.
Expand All @@ -219,6 +226,7 @@ def create(

response: Response = self.post(
self.reverse("list"),
data=data,
status_code_assertion=status_code_assertion,
**kwargs,
)
Expand All @@ -235,7 +243,7 @@ def create(
def retrieve(
self,
model: AnyModel,
status_code_assertion: StatusCodeAssertion = None,
status_code_assertion: StatusCodeAssertion = status.HTTP_200_OK,
**kwargs,
):
"""Retrieve a model.
Expand Down Expand Up @@ -265,7 +273,7 @@ def retrieve(
def list(
self,
models: t.Iterable[AnyModel],
status_code_assertion: StatusCodeAssertion = None,
status_code_assertion: StatusCodeAssertion = status.HTTP_200_OK,
filters: ListFilters = None,
**kwargs,
):
Expand Down Expand Up @@ -302,7 +310,7 @@ def partial_update(
self,
model: AnyModel,
data: Data,
status_code_assertion: StatusCodeAssertion = None,
status_code_assertion: StatusCodeAssertion = status.HTTP_200_OK,
**kwargs,
):
"""Partially update a model.
Expand Down Expand Up @@ -336,14 +344,16 @@ def partial_update(
def destroy(
self,
model: AnyModel,
status_code_assertion: StatusCodeAssertion = None,
status_code_assertion: StatusCodeAssertion = status.HTTP_204_NO_CONTENT,
anonymized: bool = False,
**kwargs,
):
"""Destroy a model.
Args:
model: The model to destroy.
status_code_assertion: The expected status code.
anonymized: Whether or not the data is anonymized.
Returns:
The HTTP response.
Expand All @@ -355,7 +365,10 @@ def destroy(
**kwargs,
)

# TODO: add standard post-destroy assertions.
if not anonymized and self.status_code_is_ok(response.status_code):
# pylint: disable-next=no-member
with self._test_case.assertRaises(model.DoesNotExist):
model.refresh_from_db()

return response

Expand All @@ -369,11 +382,15 @@ def login(self, **credentials):
if user.session.session_auth_factors.filter(
auth_factor__type=AuthFactor.Type.OTP
).exists():
request = self.request_factory.request()
request.user = user

now = timezone.now()
otp = TOTP(user.otp_secret).at(now)
with patch.object(timezone, "now", return_value=now):
assert super().login(
otp=otp
request=request,
otp=otp,
), f'Failed to login with OTP "{otp}" at {now}.'

assert user.is_authenticated, "Failed to authenticate user."
Expand Down

0 comments on commit d6c6a7e

Please sign in to comment.