Skip to content

Commit

Permalink
fix: polish tests (#113)
Browse files Browse the repository at this point in the history
* fix: add test coverage

* use xml

* rename codecov yaml

* add yml path

* ignore codeforlife test helpers

* reset pipeline

* reset pipeline

* use updated workflow

* polish_test

* add todo

* Merge branch 'main' into polish_tests

* fix: test job

* use new test workflow

* add some small tests and don't cover some lines

* no cov when debugging tests

* test get queryset

* update config

* merge from main

* use main
  • Loading branch information
SKairinos committed Apr 18, 2024
1 parent 0a49411 commit 05e17e2
Show file tree
Hide file tree
Showing 15 changed files with 127 additions and 31 deletions.
4 changes: 3 additions & 1 deletion .devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"qwtel.sqlite-viewer",
"njpwerner.autodocstring",
"tamasfe.even-better-toml",
"github.vscode-github-actions"
"github.vscode-github-actions",
"codecov.codecov",
"ritwickdey.liveserver"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"type": "debugpy"
},
{
"env": {
"PYTEST_ADDOPTS": "--no-cov"
},
"justMyCode": false,
"name": "Pytest",
"presentation": {
Expand Down
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
"python.defaultInterpreterPath": ".venv/bin/python",
"python.testing.pytestArgs": [
"-n=auto",
"--cov",
"--cov-report=html",
"-c=pyproject.toml",
"."
],
Expand Down
5 changes: 5 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ coverage:
patch:
default:
target: 90%
threshold: 1%
project:
default:
target: 90%
threshold: 1%

comment: false
25 changes: 19 additions & 6 deletions codeforlife/tests/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ def get_model_class(cls) -> t.Type[AnyModel]:
Returns:
The model's class.
"""

# pylint: disable-next=no-member
return t.get_args(cls.__orig_bases__[0])[ # type: ignore[attr-defined]
0
Expand All @@ -37,7 +36,6 @@ def assert_raises_integrity_error(self, *args, **kwargs):
Returns:
Error catcher that will assert if an integrity error is raised.
"""

return self.assertRaises(IntegrityError, *args, **kwargs)

def assert_check_constraint(self, name: str, *args, **kwargs):
Expand Down Expand Up @@ -75,11 +73,26 @@ def assert_does_not_exist(self, model_or_pk: t.Union[AnyModel, t.Any]):
Args:
model_or_pk: The model itself or its primary key.
"""

model_class = self.get_model_class()
with self.assertRaises(ObjectDoesNotExist):
if isinstance(model_or_pk, Model):
model_or_pk.refresh_from_db()
else:
objects = model_class.objects # type: ignore[attr-defined]
objects.get(pk=model_or_pk)
(
self.get_model_class().objects # type: ignore[attr-defined]
).get(pk=model_or_pk)

def assert_get_queryset(
self, values: t.Collection[AnyModel], ordered: bool = True
):
"""Assert that the expected queryset is returned.
Args:
values: The values we expect the queryset to contain.
ordered: Whether the queryset provides an implicit ordering.
"""
queryset = (
self.get_model_class().objects # type: ignore[attr-defined]
).get_queryset()
if ordered and not queryset.ordered:
queryset = queryset.order_by("pk")
self.assertQuerysetEqual(queryset, values, ordered=ordered)
4 changes: 2 additions & 2 deletions codeforlife/user/models/auth_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from .user import User

if t.TYPE_CHECKING:
if t.TYPE_CHECKING: # pragma: no cover
from .session_auth_factor import SessionAuthFactor


Expand All @@ -37,4 +37,4 @@ class Meta:
unique_together = ["user", "type"]

def __str__(self):
return self.type
return str(self.type)
15 changes: 15 additions & 0 deletions codeforlife/user/models/auth_factor_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
© Ocado Group
Created on 16/04/2024 at 14:29:21(+01:00).
"""

from ...tests import ModelTestCase
from .auth_factor import AuthFactor


# pylint: disable-next=missing-class-docstring
class TestAuthFactor(ModelTestCase[AuthFactor]):
def test_str(self):
"""String representation is as expected."""
auth_factor_type = AuthFactor.Type.OTP
assert str(AuthFactor(type=auth_factor_type)) == str(auth_factor_type)
7 changes: 3 additions & 4 deletions codeforlife/user/models/otp_bypass_token_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,16 @@ def setUp(self):
assert user
self.user = user

def test_bulk_create(self):
def test_objects__bulk_create(self):
"""Can bulk create a new set of tokens."""
original_otp_bypass_tokens = list(self.user.otp_bypass_tokens.all())

otp_bypass_tokens = OtpBypassToken.objects.bulk_create(self.user)

assert len(otp_bypass_tokens) == OtpBypassToken.max_count

for otp_bypass_token in original_otp_bypass_tokens:
self.assert_does_not_exist(otp_bypass_token)

assert len(otp_bypass_tokens) == OtpBypassToken.max_count
assert len(otp_bypass_tokens) == self.user.otp_bypass_tokens.count()

for otp_bypass_token in otp_bypass_tokens:
Expand All @@ -42,7 +41,7 @@ def test_bulk_create(self):
)
assert check_password(raw_token, otp_bypass_token.token)

def test_create(self):
def test_save(self):
"""Cannot create or update a single instance."""
with self.assert_raises_integrity_error():
OtpBypassToken().save()
Expand Down
7 changes: 2 additions & 5 deletions codeforlife/user/models/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from .user import User

if t.TYPE_CHECKING:
if t.TYPE_CHECKING: # pragma: no cover
from .session_auth_factor import SessionAuthFactor


Expand Down Expand Up @@ -81,10 +81,7 @@ def create_model_instance(self, data):
session.user = User.objects.get(id=user_id)
SessionAuthFactor.objects.bulk_create(
[
SessionAuthFactor(
session=session,
auth_factor=auth_factor,
)
SessionAuthFactor(session=session, auth_factor=auth_factor)
for auth_factor in session.user.auth_factors.all()
]
)
Expand Down
18 changes: 18 additions & 0 deletions codeforlife/user/models/session_auth_factor_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
© Ocado Group
Created on 16/04/2024 at 14:36:42(+01:00).
"""

from ...tests import ModelTestCase
from .auth_factor import AuthFactor
from .session_auth_factor import SessionAuthFactor


# pylint: disable-next=missing-class-docstring
class TestSessionAuthFactor(ModelTestCase[SessionAuthFactor]):
def test_str(self):
"""String representation is as expected."""
auth_factor_type = AuthFactor.Type.OTP
assert str(
SessionAuthFactor(auth_factor=AuthFactor(type=auth_factor_type))
) == str(auth_factor_type)
29 changes: 29 additions & 0 deletions codeforlife/user/models/session_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
© Ocado Group
Created on 16/04/2024 at 14:40:11(+01:00).
"""

from datetime import timedelta
from unittest.mock import patch

from django.utils import timezone

from ...tests import ModelTestCase
from .session import Session


# pylint: disable-next=missing-class-docstring
class TestSession(ModelTestCase[Session]):
def test_is_expired(self):
"""Can check if a session is expired."""
now = timezone.now()

session = Session(expire_date=now - timedelta(hours=1))
with patch.object(timezone, "now", return_value=now) as timezone_now:
assert session.is_expired
timezone_now.assert_called_once()

session = Session(expire_date=now + timedelta(hours=1))
with patch.object(timezone, "now", return_value=now) as timezone_now:
assert not session.is_expired
timezone_now.assert_called_once()
16 changes: 16 additions & 0 deletions codeforlife/user/models/student_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
© Ocado Group
Created on 16/04/2024 at 14:54:18(+01:00).
"""

from ...tests import ModelTestCase
from .student import Independent


# pylint: disable-next=missing-class-docstring
class TestIndependent(ModelTestCase[Independent]):
def test_objects__get_queryset(self):
"""Check if only get independent students."""
self.assert_get_queryset(
values=Independent.objects.filter(class_field__isnull=True)
)
2 changes: 1 addition & 1 deletion codeforlife/user/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .klass import Class
from .school import School

if t.TYPE_CHECKING:
if t.TYPE_CHECKING: # pragma: no cover
from .auth_factor import AuthFactor
from .otp_bypass_token import OtpBypassToken
from .session import Session
Expand Down
20 changes: 9 additions & 11 deletions codeforlife/views/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,11 @@
from .api import APIView
from .decorators import action

if t.TYPE_CHECKING:
from ..serializers import ModelListSerializer, ModelSerializer


AnyModel = t.TypeVar("AnyModel", bound=Model)

if t.TYPE_CHECKING:
if t.TYPE_CHECKING: # pragma: no cover
from ..serializers import ModelListSerializer, ModelSerializer

# NOTE: This raises an error during runtime.
# pylint: disable-next=too-few-public-methods
class _ModelViewSet(DrfModelViewSet[AnyModel], t.Generic[AnyModel]):
Expand Down Expand Up @@ -117,32 +115,32 @@ class _ModelListSerializer(

# pylint: disable=useless-parent-delegation

def destroy( # type: ignore[override]
def destroy( # type: ignore[override] # pragma: no cover
self, request: Request[RequestUser], *args, **kwargs
):
return super().destroy(request, *args, **kwargs)

def create( # type: ignore[override]
def create( # type: ignore[override] # pragma: no cover
self, request: Request[RequestUser], *args, **kwargs
):
return super().create(request, *args, **kwargs)

def list( # type: ignore[override]
def list( # type: ignore[override] # pragma: no cover
self, request: Request[RequestUser], *args, **kwargs
):
return super().list(request, *args, **kwargs)

def retrieve( # type: ignore[override]
def retrieve( # type: ignore[override] # pragma: no cover
self, request: Request[RequestUser], *args, **kwargs
):
return super().retrieve(request, *args, **kwargs)

def update( # type: ignore[override]
def update( # type: ignore[override] # pragma: no cover
self, request: Request[RequestUser], *args, **kwargs
):
return super().update(request, *args, **kwargs)

def partial_update( # type: ignore[override]
def partial_update( # type: ignore[override] # pragma: no cover
self, request: Request[RequestUser], *args, **kwargs
):
return super().partial_update(request, *args, **kwargs)
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ extend-exclude = ".*/migrations/.*py"

[tool.pytest.ini_options]
env = ["DJANGO_SETTINGS_MODULE=manage"]
addopts = "--cov --cov-report=xml"

[tool.mypy]
plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main"]
Expand Down

0 comments on commit 05e17e2

Please sign in to comment.