Skip to content

Commit

Permalink
fix: view permissions unit tests (#57)
Browse files Browse the repository at this point in the history
* use new base class

* quick save

* in_school unit tests

* tidy up

* tidy up

* add isort

* ignore migrations

* ignore all migrations

* use job

* move load plugins to pyproject.toml

* feedback
  • Loading branch information
SKairinos authored Jan 17, 2024
1 parent 765deaf commit 161666d
Show file tree
Hide file tree
Showing 19 changed files with 848 additions and 155 deletions.
10 changes: 1 addition & 9 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,7 @@ env:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [3.8]
steps:
- uses: ocadotechnology/codeforlife-workspace/.github/actions/python/test@main
with:
python-version: ${{ matrix.python-version }}
uses: ocadotechnology/codeforlife-workspace/.github/workflows/test-python-code.yaml@main

docs:
runs-on: ubuntu-latest
Expand Down
14 changes: 12 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"isort.path": [
".venv/bin/python",
"-m",
"isort"
],
"isort.args": [
"--settings-file=pyproject.toml"
],
"black-formatter.path": [
".venv/bin/python",
"-m",
Expand All @@ -13,14 +21,16 @@
"-m",
"mypy"
],
"mypy-type-checker.args": [
"--config-file=pyproject.toml"
],
"pylint.path": [
".venv/bin/python",
"-m",
"pylint"
],
"pylint.args": [
"--rcfile=pyproject.toml",
"--load-plugins=pylint_django"
"--rcfile=pyproject.toml"
],
"python.testing.pytestArgs": [
"-n=auto",
Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pylint = "==3.0.2"
pylint-django = "==2.5.5"
pygraphviz = "==1.11"
pytest-xdist = {version = "==3.5.0", extras = ["psutil"]}
isort = "==5.13.2"

[requires]
python_version = "3.8"
4 changes: 2 additions & 2 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions codeforlife/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Meta(TypedModelMeta):
class WarehouseModel(Model):
"""To be inherited by all models whose data is to be warehoused."""

class QuerySet(models.QuerySet):
class QuerySet(models.QuerySet[AnyModel], t.Generic[AnyModel]):
"""Custom queryset to support CFL's system's operations."""

model: "WarehouseModel" # type: ignore[assignment]
Expand Down Expand Up @@ -81,7 +81,7 @@ def get_queryset(self):
A warehouse query set.
"""

return WarehouseModel.QuerySet(
return WarehouseModel.QuerySet[AnyModel](
model=self.model,
using=self._db,
hints=self._hints, # type: ignore[attr-defined]
Expand All @@ -91,31 +91,31 @@ def filter(self, *args, **kwargs):
"""A stub that returns our custom queryset."""

return t.cast(
WarehouseModel.QuerySet,
WarehouseModel.QuerySet[AnyModel],
super().filter(*args, **kwargs),
)

def exclude(self, *args, **kwargs):
"""A stub that returns our custom queryset."""

return t.cast(
WarehouseModel.QuerySet,
WarehouseModel.QuerySet[AnyModel],
super().exclude(*args, **kwargs),
)

def all(self):
"""A stub that returns our custom queryset."""

return t.cast(
WarehouseModel.QuerySet,
WarehouseModel.QuerySet[AnyModel],
super().all(),
)

def none(self):
"""A stub that returns our custom queryset."""

return t.cast(
WarehouseModel.QuerySet,
WarehouseModel.QuerySet[AnyModel],
super().none(),
)

Expand Down
6 changes: 6 additions & 0 deletions codeforlife/permissions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
Created on 14/12/2023 at 14:04:57(+00:00).
"""

import typing as t

from rest_framework.permissions import BasePermission

from .is_cron_request_from_google import IsCronRequestFromGoogle
from .is_self import IsSelf

AnyPermission = t.TypeVar("AnyPermission", bound=BasePermission)
1 change: 1 addition & 0 deletions codeforlife/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
from .api import APIClient, APITestCase
from .cron import CronTestCase, CronTestClient
from .model import ModelTestCase
from .permission import PermissionTestCase
99 changes: 99 additions & 0 deletions codeforlife/tests/permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
© Ocado Group
Created on 15/01/2024 at 15:15:20(+00:00).
Test helpers for Django Rest Framework permissions.
"""

import typing as t

from django.test import TestCase
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView

from ..permissions import AnyPermission
from ..types import Args, KwArgs


class PermissionTestCase(TestCase, t.Generic[AnyPermission]):
"""Base for all permission test cases."""

@classmethod
def get_permission_class(cls) -> t.Type[AnyPermission]:
"""Get the permission's class.
Returns:
The permission's class.
"""

# pylint: disable-next=no-member
return t.get_args(cls.__orig_bases__[0])[ # type: ignore[attr-defined]
0
]

def setUp(self):
self.request_factory = APIRequestFactory()

def _has_permission(
self,
request: Request,
view: t.Optional[APIView],
init_args: t.Optional[Args],
init_kwargs: t.Optional[KwArgs],
):
view = view or APIView()
init_args = init_args or tuple()
init_kwargs = init_kwargs or {}

permission_class = self.get_permission_class()
permission = permission_class(*init_args, **init_kwargs)
return permission.has_permission(request, view)

def assert_has_permission(
self,
request: Request,
view: t.Optional[APIView] = None,
init_args: t.Optional[Args] = None,
init_kwargs: t.Optional[KwArgs] = None,
):
"""Assert that the request does have permission.
Args:
request: The request being sent to the view.
view: The view that is being requested.
init_args: The arguments used to initialize the permission.
init_kwargs: The keyword arguments used to initialize the
permission.
"""

assert self._has_permission(
request,
view,
init_args,
init_kwargs,
)

def assert_not_has_permission(
self,
request: Request,
view: t.Optional[APIView] = None,
init_args: t.Optional[Args] = None,
init_kwargs: t.Optional[KwArgs] = None,
):
"""Assert that the request does not have permission.
Args:
request: The request being sent to the view.
view: The view that is being requested.
init_args: The arguments used to initialize the permission.
init_kwargs: The keyword arguments used to initialize the
permission.
"""

assert not self._has_permission(
request,
view,
init_args,
init_kwargs,
)
15 changes: 15 additions & 0 deletions codeforlife/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
© Ocado Group
Created on 15/01/2024 at 15:32:54(+00:00).
Reusable type hints.
"""

import typing as t

Args = t.Tuple[t.Any, ...]
KwArgs = t.Dict[str, t.Any]

JsonList = t.List["JsonValue"]
JsonDict = t.Dict[str, "JsonValue"]
JsonValue = t.Union[int, str, bool, JsonList, JsonDict]
10 changes: 10 additions & 0 deletions codeforlife/user/fixtures/schools.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,15 @@
"country": "GB",
"uk_county": "Surrey"
}
},
{
"model": "user.School",
"pk": 2,
"fields": {
"last_saved_at": "2023-01-01 00:00:00.0+00:00",
"name": "Another Example School",
"country": "GB",
"uk_county": "Surrey"
}
}
]
8 changes: 8 additions & 0 deletions codeforlife/user/fixtures/teachers.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,13 @@
"is_admin": false,
"school": 1
}
},
{
"model": "user.Teacher",
"pk": 3,
"fields": {
"last_saved_at": "2023-01-01 00:00:00.0+00:00",
"is_admin": false
}
}
]
13 changes: 13 additions & 0 deletions codeforlife/user/fixtures/users.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,18 @@
"email": "[email protected]",
"password": "pbkdf2_sha256$260000$a2nFLqpwD88sOeZ7wsQskW$WIJACyDluEJWKMPsO/jawrR0sHXHmYebDJoyUihKJxU="
}
},
{
"model": "user.User",
"pk": 6,
"fields": {
"last_saved_at": "2023-01-01 00:00:00.0+00:00",
"is_active": true,
"first_name": "Albert",
"last_name": "Einstein",
"email": "[email protected]",
"password": "pbkdf2_sha256$260000$a2nFLqpwD88sOeZ7wsQskW$WIJACyDluEJWKMPsO/jawrR0sHXHmYebDJoyUihKJxU=",
"teacher": 3
}
}
]
2 changes: 1 addition & 1 deletion codeforlife/user/models/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Manager(WarehouseModel.Manager["Class"]):
pk: str # type: ignore[assignment]
students: QuerySet["_student.Student"]

id = models.CharField( # type: ignore[assignment]
id: str = models.CharField( # type: ignore[assignment]
_("identifier"),
primary_key=True,
editable=False,
Expand Down
Loading

0 comments on commit 161666d

Please sign in to comment.