import os import tempfile import pytest from pathlib import Path from fastapi.testclient import TestClient # Use temporary database for tests temp_db = tempfile.NamedTemporaryFile(suffix=".db", delete=False) os.environ["DB_PATH"] = temp_db.name os.environ["ADMIN_USERNAME"] = "testadmin" os.environ["ADMIN_PASSWORD"] = "testpass12345" from app.main import app from app import config from app.database import init_db, get_db from app.auth import hash_password, verify_password, create_user, authenticate_user, ensure_admin_user config.ADMIN_USERNAME = "testadmin" config.ADMIN_PASSWORD = "testpass12345" client = TestClient(app) def setup_module(): init_db() ensure_admin_user() def teardown_module(): try: Path(temp_db.name).unlink(missing_ok=True) except Exception: pass def test_password_hashing(): pwd = "superSecretPassword123" h, salt = hash_password(pwd) assert verify_password(pwd, h, salt) is True assert verify_password("wrongPassword", h, salt) is False def test_user_creation_and_auth(): uid = create_user("demo_user", "demo_pass_999") assert uid > 0 auth = authenticate_user("demo_user", "demo_pass_999") assert auth is not None assert auth["username"] == "demo_user" wrong = authenticate_user("demo_user", "wrong_pass") assert wrong is None def test_unauthenticated_access_blocked(): # Attempting to access protected API without session resp = client.get("/api/me") assert resp.status_code == 401 resp = client.get("/api/cards") assert resp.status_code == 401 resp = client.post("/api/cards", json={"term": "apple", "translation": "яблоко"}) assert resp.status_code == 401 def test_login_and_card_crud_workflow(): # 1. Login with testadmin resp = client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) assert resp.status_code == 200 data = resp.json() assert data["status"] == "ok" assert "user" in data # 2. Check /api/me resp = client.get("/api/me") assert resp.status_code == 200 assert resp.json()["user"]["username"] == "testadmin" # 3. Create a card card_payload = { "term": "reluctant", "translation": "неохотный, нежелающий", "context_sentence": "He was reluctant to leave the room.", "phonetic": "/rɪˈlʌktənt/", "part_of_speech": "Adjective", "definition_en": "Not wanting to take some action", "tags": "general, daily", "notes": "Remember: reluctant to do sth" } resp = client.post("/api/cards", json=card_payload) assert resp.status_code == 200 created = resp.json() card_id = created["id"] assert created["term"] == "reluctant" # 4. Get cards list resp = client.get("/api/cards") assert resp.status_code == 200 res_data = resp.json() assert res_data["total"] >= 1 assert any(c["id"] == card_id for c in res_data["cards"]) # 5. Filter cards by search resp = client.get("/api/cards?search=reluctant") assert resp.status_code == 200 assert resp.json()["total"] >= 1 # 6. Filter cards by tag resp = client.get("/api/cards?tag=general") assert resp.status_code == 200 assert resp.json()["total"] >= 1 # 7. Check tags list resp = client.get("/api/tags") assert resp.status_code == 200 tags = resp.json() tag_names = [t["tag"] for t in tags] assert "general" in tag_names assert "daily" in tag_names # 8. Update card update_payload = { "translation": "неохотный, сопротивляющийся", "notes": "Updated note" } resp = client.put(f"/api/cards/{card_id}", json=update_payload) assert resp.status_code == 200 updated = resp.json() assert updated["translation"] == "неохотный, сопротивляющийся" assert updated["notes"] == "Updated note" # 9. Check stats resp = client.get("/api/stats") assert resp.status_code == 200 stats = resp.json() assert stats["total_cards"] >= 1 assert stats["today_added"] >= 1 # 10. Delete card resp = client.delete(f"/api/cards/{card_id}") assert resp.status_code == 200 assert resp.json()["status"] == "ok" # Verify deleted resp = client.get("/api/cards") assert not any(c["id"] == card_id for c in resp.json()["cards"]) # 11. Logout resp = client.post("/api/logout") assert resp.status_code == 200 # Verify session ended resp = client.get("/api/me") assert resp.status_code == 401 def test_login_rate_limiting(): # Attempt multiple failed logins from the same client for _ in range(5): resp = client.post("/api/login", json={"username": "testadmin", "password": "wrong_password_999"}) assert resp.status_code == 401 # 6th attempt should be rate limited (429) resp = client.post("/api/login", json={"username": "testadmin", "password": "wrong_password_999"}) assert resp.status_code == 429 def test_lookup_endpoint_authenticated(): from app.auth import _login_failures _login_failures.clear() # Login first resp = client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) assert resp.status_code == 200 resp = client.get("/api/lookup?q=reluctant") assert resp.status_code == 200 data = resp.json() assert data["query"] == "reluctant" assert "translation" in data assert "phonetic" in data assert len(data["translation"]) > 0