feat(core): implement secure MVP core with dictionary search, auth, and CRUD
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user