Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Build production image
run: docker build -t ev-backend:ci ev_backend
# The workflow default working-directory is already `ev_backend`, so the
# build context is `.` (using `ev_backend` here resolved to
# ev_backend/ev_backend, which has no Dockerfile).
run: docker build -t ev-backend:ci .
4 changes: 4 additions & 0 deletions ev_backend/accounts/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class UserType(DjangoObjectType):
class Meta:
model = User
exclude = ("password",) # credential hash is never exposed
# Return `role` as its raw lowercase value ("user", "station_owner",
# "admin") rather than graphene-django's UPPERCASE choice enum — the
# mobile client parses the lowercase strings.
convert_choices_to_enum = False

favorites = graphene.List(lambda: StationType)

Expand Down
15 changes: 14 additions & 1 deletion ev_backend/accounts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
and the guarantee that credentials/OTPs are hashed and never exposed.
"""

import secrets
from datetime import timedelta
from unittest import mock

Expand Down Expand Up @@ -183,7 +184,19 @@ def setUp(self):
self.user = make_user("fay", "fay@x.com")

def test_generation_is_hashed_random_and_emailed(self):
with mock.patch("accounts.models.secrets.choice", side_effect=list("246810")):
# Make only the OTP-code digits deterministic. `secrets` is a shared
# module, so patching its `choice` also intercepts Django's
# make_password() salt generation — delegate any non-digit alphabet
# (i.e. the salt) back to the real RNG so it isn't starved.
real_choice = secrets.choice
digits = iter("246810")

def fake_choice(alphabet):
if alphabet == "0123456789":
return next(digits)
return real_choice(alphabet)

with mock.patch("accounts.models.secrets.choice", side_effect=fake_choice):
otp = PasswordResetOTP.generate_for_user(self.user)
# Stored value is a hash, not the code; the code verifies against it.
self.assertNotEqual(otp.otp_hash, "246810")
Expand Down
4 changes: 4 additions & 0 deletions ev_backend/bookings/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class BookingType(DjangoObjectType):
class Meta:
model = Booking
fields = "__all__"
# Return `status` as its raw lowercase value ("pending", "approved", …)
# instead of graphene-django's auto-generated UPPERCASE choice enum.
# The mobile client and these tests depend on the lowercase contract.
convert_choices_to_enum = False


class BookingPage(graphene.ObjectType):
Expand Down
Loading