Backend and frontend testing stacks (pytest + vitest) and a few initial tests.
This commit is contained in:
@@ -9,6 +9,7 @@ Location: `backend/`
|
|||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Create a virtualenv and install dependencies.
|
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.
|
2. Copy `backend/.env.example` to `backend/.env` and adjust values.
|
||||||
3. Run migrations and start the server.
|
3. Run migrations and start the server.
|
||||||
|
|
||||||
@@ -18,6 +19,10 @@ After migrations, you can seed demo data:
|
|||||||
|
|
||||||
- `python manage.py seed_demo`
|
- `python manage.py seed_demo`
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `pytest`
|
||||||
|
|
||||||
### Core API endpoints (current scaffold)
|
### Core API endpoints (current scaffold)
|
||||||
|
|
||||||
- `POST /api/auth/register/`
|
- `POST /api/auth/register/`
|
||||||
@@ -47,6 +52,10 @@ Location: `frontend/`
|
|||||||
1. Install dependencies via `npm install`.
|
1. Install dependencies via `npm install`.
|
||||||
2. Run `npm run dev`.
|
2. Run `npm run dev`.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `npm run test`
|
||||||
|
|
||||||
The dev server proxies `/api` to `http://localhost:8000`.
|
The dev server proxies `/api` to `http://localhost:8000`.
|
||||||
|
|
||||||
## Project Notes
|
## Project Notes
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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")
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[pytest]
|
||||||
|
DJANGO_SETTINGS_MODULE = salon_api.settings
|
||||||
|
python_files = tests.py test_*.py *_tests.py
|
||||||
|
addopts = -q
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pytest>=8.0
|
||||||
|
pytest-django>=4.8
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -14,6 +15,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import App from "./App.jsx";
|
||||||
|
|
||||||
|
|
||||||
|
describe("App", () => {
|
||||||
|
it("renders the hero copy", () => {
|
||||||
|
render(<App />);
|
||||||
|
expect(
|
||||||
|
screen.getByText("Find, compare, and book top salons near you.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
@@ -7,5 +7,9 @@ export default defineConfig({
|
|||||||
proxy: {
|
proxy: {
|
||||||
"/api": "http://localhost:8000"
|
"/api": "http://localhost:8000"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: "./src/test/setupTests.js"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user