"""CLI: python -m cyclone users {create,list,disable,reset-password,set-role}. Uses Click's CliRunner (matches the existing parse-837/parse-835 CLI tests). The full CLI group lives in ``cyclone.cli.main`` — we exercise the ``users`` subgroup through the top-level ``main`` so the wiring is real. """ from __future__ import annotations import pytest from sqlalchemy import delete, select from cyclone.auth import users from cyclone.cli import main as cli_main from cyclone.db import Session as DbSession from cyclone.db import SessionLocal, User @pytest.fixture(autouse=True) def _clear(): with SessionLocal()() as db: db.execute(delete(DbSession)) db.execute(delete(User)) db.commit() yield with SessionLocal()() as db: db.execute(delete(DbSession)) db.execute(delete(User)) db.commit() def test_create_user_via_cli(): from click.testing import CliRunner runner = CliRunner() result = runner.invoke( cli_main, [ "users", "create", "cli-user", "--role", "viewer", "--password", "clipassword1", ], input="", # don't prompt ) assert result.exit_code == 0, result.output with SessionLocal()() as db: u = users.get_by_username(db, "cli-user") assert u is not None assert u.role == "viewer" def test_list_users_via_cli(): from click.testing import CliRunner with SessionLocal()() as db: users.create(db, username="listed", password="hunter2hunter2", role="user") runner = CliRunner() result = runner.invoke(cli_main, ["users", "list"]) assert result.exit_code == 0, result.output assert "listed" in result.output def test_disable_user_via_cli(): from click.testing import CliRunner with SessionLocal()() as db: users.create(db, username="todie", password="hunter2hunter2", role="user") runner = CliRunner() result = runner.invoke(cli_main, ["users", "disable", "todie"]) assert result.exit_code == 0, result.output with SessionLocal()() as db: u = users.get_by_username(db, "todie") assert u.disabled_at is not None def test_reset_password_via_cli(): from click.testing import CliRunner with SessionLocal()() as db: users.create(db, username="pwchange", password="oldpassword1", role="user") runner = CliRunner() result = runner.invoke( cli_main, [ "users", "reset-password", "pwchange", "--password", "newpassword1", ], ) assert result.exit_code == 0, result.output with SessionLocal()() as db: u = users.get_by_username(db, "pwchange") assert users.verify_password("newpassword1", u.password_hash) def test_set_role_via_cli(): from click.testing import CliRunner with SessionLocal()() as db: users.create(db, username="promote", password="hunter2hunter2", role="viewer") runner = CliRunner() result = runner.invoke( cli_main, ["users", "set-role", "promote", "--role", "admin"], ) assert result.exit_code == 0, result.output with SessionLocal()() as db: u = users.get_by_username(db, "promote") assert u.role == "admin" def test_create_rejects_short_password(): from click.testing import CliRunner runner = CliRunner() result = runner.invoke( cli_main, [ "users", "create", "weak", "--role", "viewer", "--password", "short", ], ) # Click surfaces validation failures with a non-zero exit code. assert result.exit_code != 0, result.output with SessionLocal()() as db: rows = db.execute(select(User).where(User.username == "weak")).scalars().all() assert rows == [] def test_disable_unknown_user_exits_nonzero(): from click.testing import CliRunner runner = CliRunner() result = runner.invoke(cli_main, ["users", "disable", "ghost"]) assert result.exit_code != 0, result.output