test: migrate backend suites to pytest with module coverage gate

Split backend tests into unit and integration suites, add isolated fixtures and coverage enforcement, and update CI and test documentation. Cover current authentication, content, notification, payment, media, and admin flows; fix behaviors revealed by the new tests. Validate both suites and the per-module gate against local PostgreSQL.
This commit is contained in:
2026-09-26 16:50:29 +03:30
parent 3ec28638d1
commit ff99130938
76 changed files with 3271 additions and 1357 deletions

View File

@@ -1,85 +1,72 @@
import pytest
import json
from unittest import mock
from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from apps.users.models import User
from core.authentication import create_jwt_token
@pytest.mark.django_db
@pytest.mark.integration
class TestUsersAPIIntegrationTests:
class UsersAPIIntegrationTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username="mobile_user",
mobile="09192000001",
email="person@example.com",
password="Sup3rSecure!123",
is_mobile_verified=True,
)
def setup_method(self):
self.user = User.objects.create_user(username='mobile_user', mobile='09192000001', email='person@example.com', password='Sup3rSecure!123', is_mobile_verified=True)
def _auth(self):
return {"HTTP_AUTHORIZATION": f"Bearer {create_jwt_token(self.user)}"}
return {'HTTP_AUTHORIZATION': f'Bearer {create_jwt_token(self.user)}'}
def test_password_login_accepts_mobile_identity(self):
response = self.client.post("/api/auth/login", json.dumps({
"identifier": self.user.mobile,
"password": "Sup3rSecure!123",
}), content_type="application/json")
self.assertEqual(response.status_code, 200)
self.assertIn("access_token", response.json())
response = self.client.post('/api/auth/login', json.dumps({'identifier': self.user.mobile, 'password': 'Sup3rSecure!123'}), content_type='application/json')
assert response.status_code == 200
assert 'access_token' in response.json()
def test_profile_update_treats_supplied_email_as_identity(self):
response = self.client.put("/api/auth/profile", json.dumps({"email": "new@example.com"}),
content_type="application/json", **self._auth())
self.assertEqual(response.status_code, 200)
response = self.client.put('/api/auth/profile', json.dumps({'email': 'new@example.com'}), content_type='application/json', **self._auth())
assert response.status_code == 200
self.user.refresh_from_db()
self.assertEqual(self.user.email, "new@example.com")
self.assertTrue(self.user.is_email_verified)
assert self.user.email == 'new@example.com'
assert self.user.is_email_verified
def test_profile_update_clears_email_identity_flag(self):
response = self.client.put("/api/auth/profile", json.dumps({"email": ""}),
content_type="application/json", **self._auth())
self.assertEqual(response.status_code, 200)
response = self.client.put('/api/auth/profile', json.dumps({'email': ''}), content_type='application/json', **self._auth())
assert response.status_code == 200
self.user.refresh_from_db()
self.assertIsNone(self.user.email)
self.assertFalse(self.user.is_email_verified)
assert self.user.email is None
assert not self.user.is_email_verified
def test_profile_requires_authentication_and_returns_account_identity(self):
self.assertEqual(self.client.get("/api/auth/profile").status_code, 401)
response = self.client.get("/api/auth/profile", **self._auth())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["mobile"], self.user.mobile)
assert self.client.get('/api/auth/profile').status_code == 401
response = self.client.get('/api/auth/profile', **self._auth())
assert response.status_code == 200
assert response.json()['mobile'] == self.user.mobile
def test_profile_rejects_duplicate_email(self):
User.objects.create_user(username="second", mobile="09192000002", email="taken@example.com", password="password")
response = self.client.put("/api/auth/profile", json.dumps({"email": "taken@example.com"}),
content_type="application/json", **self._auth())
self.assertEqual(response.status_code, 400)
User.objects.create_user(username='second', mobile='09192000002', email='taken@example.com', password='password')
response = self.client.put('/api/auth/profile', json.dumps({'email': 'taken@example.com'}), content_type='application/json', **self._auth())
assert response.status_code == 400
self.user.refresh_from_db()
self.assertEqual(self.user.email, "person@example.com")
assert self.user.email == 'person@example.com'
def test_profile_picture_rejects_non_image_upload(self):
image = SimpleUploadedFile("notes.txt", b"not an image", content_type="text/plain")
response = self.client.post("/api/auth/profile/picture", {"file": image}, **self._auth())
self.assertEqual(response.status_code, 400)
image = SimpleUploadedFile('notes.txt', b'not an image', content_type='text/plain')
response = self.client.post('/api/auth/profile/picture', {'file': image}, **self._auth())
assert response.status_code == 400
def test_check_username_reports_existing_user(self):
response = self.client.get("/api/auth/check-username", {"username": self.user.username})
self.assertEqual(response.status_code, 200)
self.assertTrue(response.json()["exists"])
response = self.client.get('/api/auth/check-username', {'username': self.user.username})
assert response.status_code == 200
assert response.json()['exists']
def test_legacy_email_auth_routes_are_gone(self):
self.assertEqual(self.client.get("/api/auth/verify-email/token").status_code, 404)
self.assertEqual(self.client.post("/api/auth/resend-verification").status_code, 404)
self.assertEqual(self.client.post("/api/auth/request-password-reset").status_code, 404)
assert self.client.get('/api/auth/verify-email/token').status_code == 404
assert self.client.post('/api/auth/resend-verification').status_code == 404
assert self.client.post('/api/auth/request-password-reset').status_code == 404
@mock.patch("apps.users.api.views.reset_password_with_otp")
def test_mobile_otp_password_reset_route_remains_available(self, reset):
response = self.client.post("/api/auth/reset-password", json.dumps({
"mobile": self.user.mobile,
"code": "123456",
"new_password": "AnotherSecurePass!123",
}), content_type="application/json")
self.assertEqual(response.status_code, 200)
def test_mobile_otp_password_reset_route_remains_available(self, mocker):
reset = mocker.patch('apps.users.api.views.reset_password_with_otp')
response = self.client.post('/api/auth/reset-password', json.dumps({'mobile': self.user.mobile, 'code': '123456', 'new_password': 'AnotherSecurePass!123'}), content_type='application/json')
assert response.status_code == 200
reset.assert_called_once()
@pytest.fixture(autouse=True)
def _pytest_client(self, client):
self.client = client