Skip to content

Commit

Permalink
Add check for age to reservation create endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
matti-lamppu committed Dec 11, 2024
1 parent cfcf816 commit 8e368a3
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 5 deletions.
1 change: 1 addition & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ def CACHES(cls):
REMOVE_RECURRING_RESERVATIONS_OLDER_THAN_DAYS = 1
REMOVE_EXPIRED_APPLICATIONS_OLDER_THAN_DAYS = 365
TEXT_SEARCH_CACHE_TIME_DAYS = 30
USER_IS_ADULT_AT_AGE = 18

APPLICATION_ROUND_RESERVATION_CREATION_TIMEOUT_MINUTES = values.IntegerValue(default=10)
AFFECTING_TIME_SPANS_UPDATE_INTERVAL_MINUTES = values.IntegerValue(default=2)
Expand Down
1 change: 1 addition & 0 deletions tests/factories/reservation_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class Meta:
is_draft = False
is_archived = False
require_introduction = False
require_adult_reservee = False
require_reservation_handling = False
reservation_block_whole_day = False
can_apply_free_of_charge = False
Expand Down
94 changes: 93 additions & 1 deletion tests/test_graphql_api/test_reservation/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import freezegun
import pytest
from freezegun import freeze_time
from graphene_django_extensions.testing import parametrize_helper

from tilavarauspalvelu.enums import (
Expand All @@ -18,7 +19,14 @@
from tilavarauspalvelu.models import Reservation, ReservationUnitHierarchy
from tilavarauspalvelu.utils.helauth.clients import HelsinkiProfileClient
from tilavarauspalvelu.utils.helauth.typing import ADLoginAMR
from utils.date_utils import DEFAULT_TIMEZONE, local_datetime, local_end_of_day, local_start_of_day, next_hour
from utils.date_utils import (
DEFAULT_TIMEZONE,
local_date,
local_datetime,
local_end_of_day,
local_start_of_day,
next_hour,
)
from utils.decimal_utils import round_decimal
from utils.sentry import SentryLogger

Expand Down Expand Up @@ -1125,3 +1133,87 @@ def test_reservation__create__reservee_used_ad_login(graphql, amr, expected):

reservation = Reservation.objects.get(pk=response.first_query_object["pk"])
assert reservation.reservee_used_ad_login is expected


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__is_adult(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=True)

user = UserFactory.create(social_auth__extra_data__amr="suomi_fi", date_of_birth=local_date(2006, 1, 1))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

reservation = Reservation.objects.filter(pk=response.first_query_object["pk"]).first()
assert reservation is not None


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__is_under_age(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=True)

user = UserFactory.create(social_auth__extra_data__amr="suomi_fi", date_of_birth=local_date(2006, 1, 2))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.error_message() == "Reservation unit can only be booked by an adult reservee"


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__is_under_age__reservation_unit_allows(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=False)

user = UserFactory.create(social_auth__extra_data__amr="suomi_fi", date_of_birth=local_date(2006, 1, 2))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

reservation = Reservation.objects.filter(pk=response.first_query_object["pk"]).first()
assert reservation is not None


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__is_ad_user(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=True)

user = UserFactory.create(social_auth__extra_data__amr="helsinkiazuread", date_of_birth=local_date(2006, 1, 1))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

reservation = Reservation.objects.filter(pk=response.first_query_object["pk"]).first()
assert reservation is not None


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__no_id_token(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=True)

# We don't have an ID token, so we don't know if this is an AD user.
# Still, we have have a birthday that indicates they are an adult.
user = UserFactory.create(date_of_birth=local_date(2006, 1, 1))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

reservation = Reservation.objects.filter(pk=response.first_query_object["pk"]).first()
assert reservation is not None
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class ValidationErrorCodes(Enum):
INVALID_WEEKDAY = "INVALID_WEEKDAY"
INVALID_RECURRENCE_IN_DAY = "INVALID_RECURRENCE_IN_DAYS"
RESERVATION_TYPE_NOT_ALLOWED = "RESERVATION_TYPE_NOT_ALLOWED"
RESERVATION_ADULT_RESERVEE_REQUIRED = "ADULT_RESERVEE_REQUIRED"


class ValidationErrorWithCode(GraphQLError): # noqa: N818
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from utils.date_utils import DEFAULT_TIMEZONE

if TYPE_CHECKING:
from tilavarauspalvelu.models import User
from tilavarauspalvelu.typing import AnyUser


Expand Down Expand Up @@ -142,10 +143,12 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]:
begin = begin.astimezone(DEFAULT_TIMEZONE)
end = end.astimezone(DEFAULT_TIMEZONE)

request_user: AnyUser = self.context["request"].user
reservation_units = self._get_reservation_units(data)

sku = None
for reservation_unit in reservation_units:
self.check_if_reservee_should_be_adult(reservation_unit, request_user)
self.check_reservation_time(reservation_unit)
self.check_reservation_overlap(reservation_unit, begin, end)
self.check_reservation_duration(reservation_unit, begin, end)
Expand All @@ -164,7 +167,6 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]:
data["state"] = ReservationStateChoice.CREATED.value
data["buffer_time_before"], data["buffer_time_after"] = self._calculate_buffers(begin, end, reservation_units)

request_user: AnyUser = self.context["request"].user
data["user"] = None if request_user.is_anonymous else request_user
data["reservee_used_ad_login"] = (
False if request_user.is_anonymous else getattr(request_user.id_token, "is_ad_login", False)
Expand Down Expand Up @@ -220,3 +222,21 @@ def check_reservation_kind(self, reservation_unit: ReservationUnit) -> None:
if reservation_unit.reservation_kind == ReservationKind.SEASON:
msg = "Reservation unit is only available or seasonal booking."
raise ValidationErrorWithCode(msg, ValidationErrorCodes.RESERVATION_UNIT_TYPE_IS_SEASON)

def check_if_reservee_should_be_adult(self, reservation_unit: ReservationUnit, user: User) -> None:
if self.instance is not None:
# Only check for creation
return

if not reservation_unit.require_adult_reservee:
return

# AD users are currently never under age since we have blocked students from signing in.
if user.actions.is_ad_user:
return

if user.actions.is_of_age:
return

msg = "Reservation unit can only be booked by an adult reservee"
raise ValidationErrorWithCode(msg, ValidationErrorCodes.RESERVATION_ADULT_RESERVEE_REQUIRED)
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@
from utils.sentry import SentryLogger

if TYPE_CHECKING:
from tilavarauspalvelu.models import Reservation
from tilavarauspalvelu.typing import AnyUser, WSGIRequest


class ReservationCreateSerializer(ReservationBaseSaveSerializer):
instance: Reservation
instance: None

def validate(self, data: dict[str, Any]) -> dict[str, Any]:
data = super().validate(data)
Expand Down
27 changes: 26 additions & 1 deletion tilavarauspalvelu/models/user/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from auditlog.models import LogEntry
from dateutil.relativedelta import relativedelta
from django.conf import settings
from social_django.models import UserSocialAuth

from tilavarauspalvelu.enums import (
Expand All @@ -25,7 +26,7 @@
UnitRole,
)
from tilavarauspalvelu.typing import UserAnonymizationInfo
from utils.date_utils import local_datetime
from utils.date_utils import local_date, local_datetime

if TYPE_CHECKING:
from collections.abc import Iterable
Expand Down Expand Up @@ -187,3 +188,27 @@ def can_anonymize(self) -> UserAnonymizationInfo:
has_open_applications=has_open_applications,
has_open_payments=has_open_payments,
)

@property
def is_ad_user(self) -> bool:
id_token = self.user.id_token
return id_token is not None and id_token.is_ad_login

@property
def is_profile_user(self) -> bool:
id_token = self.user.id_token
return id_token is not None and id_token.is_profile_login

@property
def user_age(self) -> int | None:
birthday = self.user.date_of_birth
if birthday is None:
return None
return relativedelta(local_date(), birthday).years

@property
def is_of_age(self) -> bool:
user_age = self.user_age
if user_age is None:
return False
return user_age >= settings.USER_IS_ADULT_AT_AGE

0 comments on commit 8e368a3

Please sign in to comment.