From 96efdf8dab605b066081fac22b93f22941b84ab1 Mon Sep 17 00:00:00 2001 From: tewodros Date: Tue, 14 Jul 2026 11:27:25 +0300 Subject: [PATCH 1/2] fix(graphql): return lowercase status/role; fix over-broad OTP test mock CI failures were two independent issues: 1. graphene-django 3.2.3 auto-converts CharField `choices` to a GraphQL Enum whose values serialize UPPERCASE ("PENDING", "STATION_OWNER"). This broke the booking status assertions (3 failures) and silently breaks the mobile client, which parses the lowercase raw values for both Booking.status and User.role. Set `convert_choices_to_enum = False` on BookingType and UserType to return the raw lowercase values. No test asserts the uppercase form and status/role are only ever passed as quoted string inputs, so inputs are unaffected. 2. OtpModelTests mocked `accounts.models.secrets.choice`, but `secrets` is a shared module, so the patch also intercepted Django make_password()'s salt generation and starved the 6-value side_effect -> RuntimeError (StopIteration under PEP 479 / Python 3.12). The mock now only fixes the OTP digit alphabet and delegates any other alphabet (the salt) to the real RNG. Co-Authored-By: Claude Opus 4.8 --- ev_backend/accounts/schema.py | 4 ++++ ev_backend/accounts/tests.py | 15 ++++++++++++++- ev_backend/bookings/schema.py | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ev_backend/accounts/schema.py b/ev_backend/accounts/schema.py index 26e30ed..7dee8db 100644 --- a/ev_backend/accounts/schema.py +++ b/ev_backend/accounts/schema.py @@ -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) diff --git a/ev_backend/accounts/tests.py b/ev_backend/accounts/tests.py index 874d64c..2165eca 100644 --- a/ev_backend/accounts/tests.py +++ b/ev_backend/accounts/tests.py @@ -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 @@ -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") diff --git a/ev_backend/bookings/schema.py b/ev_backend/bookings/schema.py index 16e768d..b853082 100644 --- a/ev_backend/bookings/schema.py +++ b/ev_backend/bookings/schema.py @@ -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): From 26556aa4b6a33ab0ffe8a05f7dfd7955c14a5c7b Mon Sep 17 00:00:00 2001 From: tewodros Date: Tue, 14 Jul 2026 11:33:48 +0300 Subject: [PATCH 2/2] fix(ci): correct docker build context in the build job The workflow sets `defaults.run.working-directory: ev_backend` for every run step, so `docker build ... ev_backend` resolved the context to ev_backend/ev_backend (the inner settings package, which has no Dockerfile): failed to read dockerfile: open Dockerfile: no such file or directory The tracked Dockerfile lives at ev_backend/Dockerfile, so with the step already running inside ev_backend the context must be `.`. Verified the Dockerfile and .dockerignore are coherent for a context of `.` (requirements.txt, entrypoint.sh, manage.py and the app packages are all present and un-ignored). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9b14bc..c069975 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 .