إنجاز: تفعيل خاصية استعادة كلمة المرور وإعادة تعيينها
- إضافة نقاط نهاية لخاصيتي استعادة كلمة المرور وإعادة تعيينها في الواجهة الخلفية. - إنشاء اختبارات لخاصية إعادة تعيين كلمة المرور لضمان كفاءتها وأمانها. - تطوير صفحات واجهة المستخدم لخاصيتي استعادة كلمة المرور وإعادة تعيينها مع معالجة النماذج. - دمج حدود تحميل ديناميكية لأنواع ملفات مختلفة بناءً على خطط المستخدمين. - تقديم أداة جديدة لتغيير حجم الصور مع إمكانية تعديل الأبعاد وإعدادات الجودة. - تحديث نظام التوجيه والتنقل ليشمل أدوات جديدة وميزات مصادقة. - تحسين تجربة المستخدم من خلال معالجة الأخطاء ورسائل التغذية الراجعة المناسبة. - إضافة دعم التدويل للميزات الجديدة باللغات الإنجليزية والعربية والفرنسية.
This commit is contained in:
53
backend/tests/test_config.py
Normal file
53
backend/tests/test_config.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Tests for GET /api/config — dynamic upload limits."""
|
||||
import pytest
|
||||
|
||||
|
||||
class TestConfigEndpoint:
|
||||
"""Tests for the public config endpoint."""
|
||||
|
||||
def test_anonymous_gets_free_limits(self, client):
|
||||
"""Anonymous users receive free-plan file limits."""
|
||||
resp = client.get("/api/config")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
|
||||
assert "file_limits_mb" in data
|
||||
assert "max_upload_mb" in data
|
||||
limits = data["file_limits_mb"]
|
||||
assert limits["pdf"] == 20
|
||||
assert limits["word"] == 15
|
||||
assert limits["image"] == 10
|
||||
assert limits["video"] == 50
|
||||
assert limits["homepageSmartUpload"] == 50
|
||||
# No usage section for anon
|
||||
assert "usage" not in data
|
||||
|
||||
def test_authenticated_free_user_gets_usage(self, client, app):
|
||||
"""Logged-in free user receives limits + usage summary."""
|
||||
# Register + login
|
||||
client.post("/api/auth/register", json={
|
||||
"email": "config_test@example.com",
|
||||
"password": "TestPassword123!",
|
||||
})
|
||||
client.post("/api/auth/login", json={
|
||||
"email": "config_test@example.com",
|
||||
"password": "TestPassword123!",
|
||||
})
|
||||
|
||||
resp = client.get("/api/config")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
|
||||
assert data["file_limits_mb"]["pdf"] == 20
|
||||
assert "usage" in data
|
||||
usage = data["usage"]
|
||||
assert usage["plan"] == "free"
|
||||
assert "web_quota" in usage
|
||||
assert "api_quota" in usage
|
||||
|
||||
def test_max_upload_mb_is_correct(self, client):
|
||||
"""max_upload_mb should equal the largest single-type limit."""
|
||||
resp = client.get("/api/config")
|
||||
data = resp.get_json()
|
||||
limits = data["file_limits_mb"]
|
||||
assert data["max_upload_mb"] == max(limits.values())
|
||||
116
backend/tests/test_maintenance_tasks.py
Normal file
116
backend/tests/test_maintenance_tasks.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Tests for the cleanup_expired_files periodic maintenance task."""
|
||||
import os
|
||||
import time
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.tasks.maintenance_tasks import _cleanup_dir
|
||||
|
||||
|
||||
class TestCleanupDir:
|
||||
"""Tests for _cleanup_dir helper."""
|
||||
|
||||
def test_returns_zeros_for_missing_directory(self):
|
||||
stats = _cleanup_dir("/no/such/path", 1800)
|
||||
assert stats == {"scanned": 0, "deleted": 0, "freed_bytes": 0, "errors": 0}
|
||||
|
||||
def test_skips_files_in_root(self, tmp_path):
|
||||
"""Regular files in the root should be ignored (only dirs scanned)."""
|
||||
(tmp_path / "regular.txt").write_text("hello")
|
||||
stats = _cleanup_dir(str(tmp_path), 1800)
|
||||
assert stats["scanned"] == 0
|
||||
assert stats["deleted"] == 0
|
||||
|
||||
def test_keeps_recent_directory(self, tmp_path):
|
||||
"""Directories younger than expiry should remain untouched."""
|
||||
sub = tmp_path / "recent_job"
|
||||
sub.mkdir()
|
||||
(sub / "file.pdf").write_bytes(b"%PDF-1.4 test")
|
||||
stats = _cleanup_dir(str(tmp_path), 1800)
|
||||
assert stats["scanned"] == 1
|
||||
assert stats["deleted"] == 0
|
||||
assert sub.exists()
|
||||
|
||||
def test_deletes_expired_directory(self, tmp_path):
|
||||
"""Directories older than expiry should be removed."""
|
||||
sub = tmp_path / "old_job"
|
||||
sub.mkdir()
|
||||
(sub / "file.pdf").write_bytes(b"%PDF-1.4 test")
|
||||
# Set mtime to 1 hour ago
|
||||
old_time = time.time() - 3600
|
||||
os.utime(str(sub), (old_time, old_time))
|
||||
|
||||
stats = _cleanup_dir(str(tmp_path), 1800)
|
||||
assert stats["scanned"] == 1
|
||||
assert stats["deleted"] == 1
|
||||
assert stats["freed_bytes"] > 0
|
||||
assert not sub.exists()
|
||||
|
||||
def test_counts_freed_bytes(self, tmp_path):
|
||||
"""Freed bytes should approximately match the size of deleted files."""
|
||||
sub = tmp_path / "old_job"
|
||||
sub.mkdir()
|
||||
content = b"A" * 4096
|
||||
(sub / "data.bin").write_bytes(content)
|
||||
old_time = time.time() - 3600
|
||||
os.utime(str(sub), (old_time, old_time))
|
||||
|
||||
stats = _cleanup_dir(str(tmp_path), 1800)
|
||||
assert stats["freed_bytes"] >= 4096
|
||||
|
||||
def test_mixed_old_and_new(self, tmp_path):
|
||||
"""Only expired directories are deleted, recent ones kept."""
|
||||
old = tmp_path / "expired_dir"
|
||||
old.mkdir()
|
||||
(old / "a.txt").write_text("old")
|
||||
old_time = time.time() - 7200
|
||||
os.utime(str(old), (old_time, old_time))
|
||||
|
||||
recent = tmp_path / "fresh_dir"
|
||||
recent.mkdir()
|
||||
(recent / "b.txt").write_text("new")
|
||||
|
||||
stats = _cleanup_dir(str(tmp_path), 1800)
|
||||
assert stats["scanned"] == 2
|
||||
assert stats["deleted"] == 1
|
||||
assert not old.exists()
|
||||
assert recent.exists()
|
||||
|
||||
|
||||
class TestCleanupExpiredFilesTask:
|
||||
"""Integration test for the Celery task via direct invocation."""
|
||||
|
||||
def test_task_runs_and_returns_stats(self, app):
|
||||
"""Task should return a summary dict."""
|
||||
# Create an expired directory in uploads
|
||||
upload_dir = app.config["UPLOAD_FOLDER"]
|
||||
expired = os.path.join(upload_dir, "expired_session")
|
||||
os.makedirs(expired, exist_ok=True)
|
||||
with open(os.path.join(expired, "test.pdf"), "wb") as f:
|
||||
f.write(b"%PDF-TEST")
|
||||
old_time = time.time() - 7200
|
||||
os.utime(expired, (old_time, old_time))
|
||||
|
||||
with app.app_context():
|
||||
from app.tasks.maintenance_tasks import cleanup_expired_files
|
||||
result = cleanup_expired_files()
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["deleted"] >= 1
|
||||
assert result["freed_bytes"] > 0
|
||||
assert not os.path.exists(expired)
|
||||
|
||||
def test_task_leaves_recent_alone(self, app):
|
||||
"""Task should not delete recent directories."""
|
||||
upload_dir = app.config["UPLOAD_FOLDER"]
|
||||
recent = os.path.join(upload_dir, "recent_session")
|
||||
os.makedirs(recent, exist_ok=True)
|
||||
with open(os.path.join(recent, "test.pdf"), "wb") as f:
|
||||
f.write(b"%PDF-TEST")
|
||||
|
||||
with app.app_context():
|
||||
from app.tasks.maintenance_tasks import cleanup_expired_files
|
||||
result = cleanup_expired_files()
|
||||
|
||||
assert result["deleted"] == 0
|
||||
assert os.path.exists(recent)
|
||||
132
backend/tests/test_password_reset.py
Normal file
132
backend/tests/test_password_reset.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Tests for forgot-password and reset-password endpoints."""
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestForgotPassword:
|
||||
"""Tests for POST /api/auth/forgot-password."""
|
||||
|
||||
def test_forgot_password_returns_200_for_unknown_email(self, client):
|
||||
"""Should always return 200 to avoid leaking registration info."""
|
||||
resp = client.post("/api/auth/forgot-password", json={
|
||||
"email": "doesnotexist@example.com",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "message" in resp.get_json()
|
||||
|
||||
def test_forgot_password_returns_200_for_registered_email(self, client):
|
||||
"""Should return 200 and trigger email sending."""
|
||||
client.post("/api/auth/register", json={
|
||||
"email": "reset_user@example.com",
|
||||
"password": "TestPassword123!",
|
||||
})
|
||||
client.post("/api/auth/logout")
|
||||
|
||||
with patch("app.routes.auth.send_password_reset_email") as mock_send:
|
||||
mock_send.return_value = True
|
||||
resp = client.post("/api/auth/forgot-password", json={
|
||||
"email": "reset_user@example.com",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
mock_send.assert_called_once()
|
||||
|
||||
def test_forgot_password_bad_email_format(self, client):
|
||||
"""Still returns 200 even for bad email format (no info leak)."""
|
||||
resp = client.post("/api/auth/forgot-password", json={
|
||||
"email": "not-an-email",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
class TestResetPassword:
|
||||
"""Tests for POST /api/auth/reset-password."""
|
||||
|
||||
def test_reset_password_missing_token(self, client):
|
||||
"""Should reject when token is empty."""
|
||||
resp = client.post("/api/auth/reset-password", json={
|
||||
"token": "",
|
||||
"password": "NewPassword123!",
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_reset_password_invalid_token(self, client):
|
||||
"""Should reject unknown token."""
|
||||
resp = client.post("/api/auth/reset-password", json={
|
||||
"token": "totally-invalid-token",
|
||||
"password": "NewPassword123!",
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_reset_password_short_password(self, client):
|
||||
"""Should reject short passwords."""
|
||||
resp = client.post("/api/auth/reset-password", json={
|
||||
"token": "some-token",
|
||||
"password": "short",
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_reset_password_full_flow(self, client, app):
|
||||
"""Register → forgot → get token → reset → login with new password."""
|
||||
# Register
|
||||
client.post("/api/auth/register", json={
|
||||
"email": "fullreset@example.com",
|
||||
"password": "OldPassword123!",
|
||||
})
|
||||
client.post("/api/auth/logout")
|
||||
|
||||
# Create reset token directly
|
||||
from app.services.account_service import get_user_by_email, create_password_reset_token
|
||||
|
||||
with app.app_context():
|
||||
user = get_user_by_email("fullreset@example.com")
|
||||
token = create_password_reset_token(user["id"])
|
||||
|
||||
# Reset
|
||||
resp = client.post("/api/auth/reset-password", json={
|
||||
"token": token,
|
||||
"password": "NewPassword123!",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Login with new password
|
||||
resp = client.post("/api/auth/login", json={
|
||||
"email": "fullreset@example.com",
|
||||
"password": "NewPassword123!",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Old password should fail
|
||||
client.post("/api/auth/logout")
|
||||
resp = client.post("/api/auth/login", json={
|
||||
"email": "fullreset@example.com",
|
||||
"password": "OldPassword123!",
|
||||
})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_reset_token_cannot_be_reused(self, client, app):
|
||||
"""A reset token should be consumed on use and fail on second use."""
|
||||
client.post("/api/auth/register", json={
|
||||
"email": "reuse@example.com",
|
||||
"password": "OldPassword123!",
|
||||
})
|
||||
client.post("/api/auth/logout")
|
||||
|
||||
from app.services.account_service import get_user_by_email, create_password_reset_token
|
||||
|
||||
with app.app_context():
|
||||
user = get_user_by_email("reuse@example.com")
|
||||
token = create_password_reset_token(user["id"])
|
||||
|
||||
# First use — should succeed
|
||||
resp = client.post("/api/auth/reset-password", json={
|
||||
"token": token,
|
||||
"password": "NewPassword123!",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Second use — should fail
|
||||
resp = client.post("/api/auth/reset-password", json={
|
||||
"token": token,
|
||||
"password": "AnotherPassword123!",
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
Reference in New Issue
Block a user