From be2590d7f73f6e14aca4a0f20f3d7c5b8ddd9847 Mon Sep 17 00:00:00 2001 From: mohammad Date: Fri, 27 Feb 2026 16:03:06 +0300 Subject: [PATCH] Backend and frontend testing stacks (pytest + vitest) and a few initial tests. --- README.md | 9 ++++++ .../apps/accounts/tests/test_otp_limits.py | 13 ++++++++ backend/apps/accounts/tests/test_phone.py | 21 +++++++++++++ .../apps/accounts/tests/test_phone_auth.py | 31 +++++++++++++++++++ backend/pytest.ini | 4 +++ backend/requirements-dev.txt | 2 ++ frontend/package.json | 9 ++++-- frontend/src/App.test.jsx | 12 +++++++ frontend/src/test/setupTests.js | 1 + frontend/vite.config.js | 4 +++ 10 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 backend/apps/accounts/tests/test_otp_limits.py create mode 100644 backend/apps/accounts/tests/test_phone.py create mode 100644 backend/apps/accounts/tests/test_phone_auth.py create mode 100644 backend/pytest.ini create mode 100644 backend/requirements-dev.txt create mode 100644 frontend/src/App.test.jsx create mode 100644 frontend/src/test/setupTests.js diff --git a/README.md b/README.md index be99211..bd77808 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Location: `backend/` ### Setup 1. Create a virtualenv and install dependencies. + - `pip install -r backend/requirements.txt -r backend/requirements-dev.txt` 2. Copy `backend/.env.example` to `backend/.env` and adjust values. 3. Run migrations and start the server. @@ -18,6 +19,10 @@ After migrations, you can seed demo data: - `python manage.py seed_demo` +### Tests + +- `pytest` + ### Core API endpoints (current scaffold) - `POST /api/auth/register/` @@ -47,6 +52,10 @@ Location: `frontend/` 1. Install dependencies via `npm install`. 2. Run `npm run dev`. +### Tests + +- `npm run test` + The dev server proxies `/api` to `http://localhost:8000`. ## Project Notes diff --git a/backend/apps/accounts/tests/test_otp_limits.py b/backend/apps/accounts/tests/test_otp_limits.py new file mode 100644 index 0000000..4660dc8 --- /dev/null +++ b/backend/apps/accounts/tests/test_otp_limits.py @@ -0,0 +1,13 @@ +import pytest +from django.test import override_settings + +from apps.accounts.models import OtpChannel +from apps.accounts.services.otp import OtpRateLimitError, create_and_send_otp + + +@pytest.mark.django_db +@override_settings(OTP_MAX_PER_WINDOW=1, OTP_WINDOW_MINUTES=15, OTP_RESEND_COOLDOWN_SECONDS=0) +def test_otp_rate_limit(): + create_and_send_otp("+966512345678", OtpChannel.SMS) + with pytest.raises(OtpRateLimitError): + create_and_send_otp("+966512345678", OtpChannel.SMS) diff --git a/backend/apps/accounts/tests/test_phone.py b/backend/apps/accounts/tests/test_phone.py new file mode 100644 index 0000000..dc661b1 --- /dev/null +++ b/backend/apps/accounts/tests/test_phone.py @@ -0,0 +1,21 @@ +import pytest + +from apps.accounts.services.phone import normalize_phone_number + + +@pytest.mark.parametrize( + "raw,expected", + [ + ("+966512345678", "+966512345678"), + ("0512345678", "+966512345678"), + ("512345678", "+966512345678"), + ("00966512345678", "+966512345678"), + ], +) +def test_normalize_phone_number_valid(raw, expected): + assert normalize_phone_number(raw) == expected + + +def test_normalize_phone_number_invalid(): + with pytest.raises(ValueError): + normalize_phone_number("12345") diff --git a/backend/apps/accounts/tests/test_phone_auth.py b/backend/apps/accounts/tests/test_phone_auth.py new file mode 100644 index 0000000..d2a4ec7 --- /dev/null +++ b/backend/apps/accounts/tests/test_phone_auth.py @@ -0,0 +1,31 @@ +import pytest +from django.urls import reverse + +from apps.accounts.models import PhoneOTP, User + + +@pytest.mark.django_db +def test_phone_auth_creates_user_and_issues_tokens(client): + request_url = reverse("phone_auth_request") + verify_url = reverse("phone_auth_verify") + + response = client.post( + request_url, + {"phone_number": "0512345678", "channel": "sms", "first_name": "Sara"}, + content_type="application/json", + ) + assert response.status_code == 201 + request_id = response.json()["request_id"] + + otp = PhoneOTP.objects.filter(phone_number="+966512345678").order_by("-created_at").first() + assert otp is not None + assert str(otp.id) == request_id + + bad = client.post( + verify_url, + {"request_id": request_id, "code": "000000"}, + content_type="application/json", + ) + assert bad.status_code == 400 + + assert User.objects.filter(phone_number="+966512345678").exists() diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..6db2981 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_SETTINGS_MODULE = salon_api.settings +python_files = tests.py test_*.py *_tests.py +addopts = -q diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 0000000..d071c31 --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest>=8.0 +pytest-django>=4.8 diff --git a/frontend/package.json b/frontend/package.json index 1dea190..02ce10b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest" }, "dependencies": { "react": "^18.2.0", @@ -14,6 +15,10 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.2.0", - "vite": "^5.0.0" + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", + "jsdom": "^24.0.0", + "vite": "^5.0.0", + "vitest": "^1.3.1" } } diff --git a/frontend/src/App.test.jsx b/frontend/src/App.test.jsx new file mode 100644 index 0000000..ac62629 --- /dev/null +++ b/frontend/src/App.test.jsx @@ -0,0 +1,12 @@ +import { render, screen } from "@testing-library/react"; +import App from "./App.jsx"; + + +describe("App", () => { + it("renders the hero copy", () => { + render(); + expect( + screen.getByText("Find, compare, and book top salons near you.") + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/test/setupTests.js b/frontend/src/test/setupTests.js new file mode 100644 index 0000000..d0de870 --- /dev/null +++ b/frontend/src/test/setupTests.js @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 3fde111..0b5a8ca 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -7,5 +7,9 @@ export default defineConfig({ proxy: { "/api": "http://localhost:8000" } + }, + test: { + environment: "jsdom", + setupFiles: "./src/test/setupTests.js" } });