feat(auth): users module with bcrypt hashing + CRUD

This commit is contained in:
Nora
2026-06-22 14:19:29 -06:00
parent 982ac127b3
commit 4e8e03363f
3 changed files with 173 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
"""Unit tests for cyclone.auth.users — User CRUD + bcrypt hashing."""
from __future__ import annotations
import pytest
from sqlalchemy import delete
from cyclone.auth import users
from cyclone.db import SessionLocal, User
@pytest.fixture(autouse=True)
def _clear_users():
with SessionLocal()() as db:
db.execute(delete(User))
db.commit()
yield
with SessionLocal()() as db:
db.execute(delete(User))
db.commit()
def test_hash_password_returns_bcrypt():
h = users.hash_password("hunter2hunter2")
assert h.startswith("$2")
def test_hash_password_produces_unique_salts():
a = users.hash_password("same-password")
b = users.hash_password("same-password")
assert a != b
def test_verify_password_correct():
h = users.hash_password("hunter2hunter2")
assert users.verify_password("hunter2hunter2", h) is True
def test_verify_password_incorrect():
h = users.hash_password("hunter2hunter2")
assert users.verify_password("WRONG", h) is False
def test_create_user_persists_with_hashed_password():
with SessionLocal()() as db:
u = users.create(db, username="alice", password="hunter2hunter2", role="admin")
assert u.id is not None
assert u.username == "alice"
assert u.role == "admin"
assert u.disabled_at is None
assert u.password_hash != "hunter2hunter2"
assert u.password_hash.startswith("$2")
def test_create_user_rejects_duplicate_username():
with SessionLocal()() as db:
users.create(db, username="bob", password="hunter2hunter2", role="user")
with SessionLocal()() as db:
with pytest.raises(Exception):
users.create(db, username="bob", password="anotherone", role="user")
def test_get_by_username_returns_user():
with SessionLocal()() as db:
users.create(db, username="carol", password="hunter2hunter2", role="viewer")
with SessionLocal()() as db:
u = users.get_by_username(db, "carol")
assert u is not None
assert u.username == "carol"
def test_get_by_username_returns_none_for_missing():
with SessionLocal()() as db:
assert users.get_by_username(db, "ghost") is None
def test_disable_user_sets_disabled_at():
with SessionLocal()() as db:
u = users.create(db, username="dave", password="hunter2hunter2", role="user")
users.disable(db, u.id)
with SessionLocal()() as db:
refreshed = users.get_by_username(db, "dave")
assert refreshed.disabled_at is not None
def test_to_public_shape_omits_password_hash():
with SessionLocal()() as db:
u = users.create(db, username="eve", password="hunter2hunter2", role="admin")
shape = users.to_public(u)
assert "password_hash" not in shape
assert shape["username"] == "eve"
assert shape["role"] == "admin"
assert "id" in shape and "createdAt" in shape