"""Unit tests for cyclone.auth.users — User CRUD + bcrypt hashing.""" from __future__ import annotations import pytest from sqlalchemy import delete from sqlalchemy.exc import IntegrityError 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(IntegrityError): 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 def test_update_password_actually_rehashes_and_verifies(): with SessionLocal()() as db: u = users.create(db, username="frank", password="hunter2hunter2", role="user") with SessionLocal()() as db: users.update_password(db, u.id, "newpassword1") # Old password no longer verifies. with SessionLocal()() as db: refreshed = users.get_by_username(db, "frank") assert not users.verify_password("hunter2hunter2", refreshed.password_hash) assert users.verify_password("newpassword1", refreshed.password_hash)