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 . 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):