Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
import uuid
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from httpx import AsyncClient
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.auth.passwords import hash_password, verify_password
|
|
from app.auth.sessions import COOKIE_NAME
|
|
from app.models import AuthSession, User
|
|
|
|
|
|
def test_password_hash_roundtrip() -> None:
|
|
hashed = hash_password("secret123")
|
|
assert hashed != "secret123"
|
|
assert verify_password(hashed, "secret123")
|
|
assert not verify_password(hashed, "wrong")
|
|
assert not verify_password("not-a-hash", "secret123")
|
|
|
|
|
|
async def _session_count(db: AsyncSession) -> int:
|
|
return (await db.execute(select(func.count(AuthSession.id)))).scalar_one()
|
|
|
|
|
|
async def test_login_success_sets_cookie_and_session(
|
|
client: AsyncClient, db: AsyncSession, seeded_user: User
|
|
) -> None:
|
|
response = await client.post(
|
|
"/api/auth/login",
|
|
json={"email": "pablo@test.dev", "password": "secret123"},
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["email"] == "pablo@test.dev"
|
|
assert body["role"] == "member"
|
|
assert COOKIE_NAME in response.cookies
|
|
assert await _session_count(db) == 1
|
|
|
|
|
|
async def test_login_normalizes_email(client: AsyncClient, seeded_user: User) -> None:
|
|
response = await client.post(
|
|
"/api/auth/login",
|
|
json={"email": " PABLO@test.dev ", "password": "secret123"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
async def test_login_wrong_password(
|
|
client: AsyncClient, db: AsyncSession, seeded_user: User
|
|
) -> None:
|
|
response = await client.post(
|
|
"/api/auth/login",
|
|
json={"email": "pablo@test.dev", "password": "wrong"},
|
|
)
|
|
assert response.status_code == 401
|
|
assert response.json() == {
|
|
"detail": "Invalid email or password.",
|
|
"code": "invalid_credentials",
|
|
}
|
|
assert await _session_count(db) == 0
|
|
|
|
|
|
async def test_login_unknown_email_same_error(client: AsyncClient) -> None:
|
|
response = await client.post(
|
|
"/api/auth/login",
|
|
json={"email": "ghost@test.dev", "password": "secret123"},
|
|
)
|
|
assert response.status_code == 401
|
|
assert response.json()["code"] == "invalid_credentials"
|
|
|
|
|
|
async def test_me_without_cookie(client: AsyncClient) -> None:
|
|
response = await client.get("/api/auth/me")
|
|
assert response.status_code == 401
|
|
assert response.json()["code"] == "not_authenticated"
|
|
|
|
|
|
async def test_me_with_garbage_cookie(client: AsyncClient) -> None:
|
|
client.cookies.set(COOKIE_NAME, "not-a-uuid")
|
|
response = await client.get("/api/auth/me")
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_me_after_login(client: AsyncClient, seeded_user: User) -> None:
|
|
await client.post(
|
|
"/api/auth/login",
|
|
json={"email": "pablo@test.dev", "password": "secret123"},
|
|
)
|
|
response = await client.get("/api/auth/me")
|
|
assert response.status_code == 200
|
|
assert response.json()["id"] == str(seeded_user.id)
|
|
|
|
|
|
async def test_logout_deletes_session(
|
|
client: AsyncClient, db: AsyncSession, seeded_user: User
|
|
) -> None:
|
|
await client.post(
|
|
"/api/auth/login",
|
|
json={"email": "pablo@test.dev", "password": "secret123"},
|
|
)
|
|
assert await _session_count(db) == 1
|
|
|
|
response = await client.post("/api/auth/logout")
|
|
assert response.status_code == 204
|
|
assert await _session_count(db) == 0
|
|
|
|
response = await client.get("/api/auth/me")
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_expired_session_rejected(
|
|
client: AsyncClient, db: AsyncSession, seeded_user: User
|
|
) -> None:
|
|
session = AuthSession(
|
|
id=uuid.uuid4(),
|
|
user_id=seeded_user.id,
|
|
expires_at=datetime.now(UTC) - timedelta(minutes=1),
|
|
)
|
|
db.add(session)
|
|
await db.commit()
|
|
|
|
client.cookies.set(COOKIE_NAME, str(session.id))
|
|
response = await client.get("/api/auth/me")
|
|
assert response.status_code == 401
|
|
assert response.json()["code"] == "not_authenticated"
|