Files
lnbits/tests/unit/test_services_users.py
T
2026-03-31 09:48:43 +03:00

408 lines
13 KiB
Python

from uuid import uuid4
import pytest
from lnbits.core.crud import (
create_account,
create_admin_settings,
create_user_extension,
delete_admin_settings,
get_account,
get_super_settings,
get_user_extensions,
get_wallets,
)
from lnbits.core.crud.settings import get_settings_field, set_settings_field
from lnbits.core.models import Account
from lnbits.core.models.extensions import UserExtension
from lnbits.core.models.users import RegisterUser
from lnbits.core.services.settings import update_cached_settings
from lnbits.core.services.users import (
check_admin_settings,
check_register_activation_settings,
create_user_account,
create_user_account_no_ckeck,
init_admin_settings,
update_user_account,
update_user_extensions,
)
from lnbits.settings import Settings
@pytest.mark.anyio
async def test_create_user_account_rejects_when_registration_disabled(
settings: Settings,
):
settings.lnbits_allow_new_accounts = False
with pytest.raises(ValueError, match="Account creation is disabled."):
await create_user_account()
@pytest.mark.anyio
@pytest.mark.parametrize(
("existing_data", "new_data", "message"),
[
(
{"username": f"user_{uuid4().hex[:8]}"},
{"username": lambda existing: existing.username},
"Username already exists.",
),
(
{"email": f"{uuid4().hex[:8]}@example.com"},
{"email": lambda existing: existing.email},
"Email already exists.",
),
(
{"pubkey": f"{1:064x}"},
{"pubkey": lambda existing: existing.pubkey},
"Pubkey already exists.",
),
],
)
async def test_create_user_account_no_check_rejects_duplicate_identity_fields(
existing_data: dict, new_data: dict, message: str
):
existing = _account(**existing_data)
await create_account(existing)
resolved = {
key: (value(existing) if callable(value) else value)
for key, value in new_data.items()
}
account = _account(**resolved)
with pytest.raises(ValueError, match=message):
await create_user_account_no_ckeck(account)
@pytest.mark.anyio
async def test_create_user_account_no_check_creates_wallet_and_extensions(
settings: Settings,
):
account = _account()
original_default_exts = list(settings.lnbits_user_default_extensions)
try:
settings.lnbits_user_default_extensions = ["default-ext"]
user = await create_user_account_no_ckeck(
account,
wallet_name="Primary",
default_exts=["extra-ext"],
)
finally:
settings.lnbits_user_default_extensions = original_default_exts
wallets = await get_wallets(user.id)
user_extensions = await get_user_extensions(user.id)
assert len(wallets) == 1
assert wallets[0].name == "Primary"
assert {ext.extension for ext in user_extensions} == {"default-ext", "extra-ext"}
assert all(ext.active is True for ext in user_extensions)
# TDOO: revisit for postgres
# @pytest.mark.anyio
# async def test_create_user_account_no_check_duplicate_extension_insert_behavior(
# settings: Settings,
# ):
# account = _account()
# original_default_exts = list(settings.lnbits_user_default_extensions)
# try:
# settings.lnbits_user_default_extensions = ["dup-ext"]
# if DB_TYPE == POSTGRES:
# with pytest.raises(DBAPIError, match="current transaction is aborted"):
# await create_user_account_no_ckeck(account, default_exts=["dup-ext"])
# else:
# user = await
# create_user_account_no_ckeck(account, default_exts=["dup-ext"])
# finally:
# settings.lnbits_user_default_extensions = original_default_exts
# if DB_TYPE == POSTGRES:
# assert await get_account(account.id) is not None
# else:
# user_extensions = await get_user_extensions(user.id)
# assert [ext.extension for ext in user_extensions] == ["dup-ext"]
@pytest.mark.anyio
async def test_update_user_account_requires_existing_user():
account = _account()
with pytest.raises(ValueError, match="User does not exist."):
await update_user_account(account)
@pytest.mark.anyio
async def test_update_user_account_rejects_conflicting_identity_fields():
existing = _account(pubkey=_pubkey(2))
conflict = _account(pubkey=_pubkey(3))
await create_account(existing)
await create_account(conflict)
with pytest.raises(ValueError, match="Username already exists."):
await update_user_account(
_account(
id_=existing.id,
username=conflict.username,
email=existing.email,
pubkey=existing.pubkey,
)
)
with pytest.raises(ValueError, match="Email already exists."):
await update_user_account(
_account(
id_=existing.id,
username=existing.username,
email=conflict.email,
pubkey=existing.pubkey,
)
)
with pytest.raises(ValueError, match="Pubkey already exists."):
await update_user_account(
_account(
id_=existing.id,
username=existing.username,
email=existing.email,
pubkey=conflict.pubkey,
)
)
@pytest.mark.anyio
async def test_update_user_account_updates_persisting_password():
account = _account(pubkey=_pubkey(4))
account.hash_password("secret1234")
await create_account(account)
updated = _account(
id_=account.id,
username=f"updated_{account.id[:8]}",
email=f"{account.id[:8]}+updated@example.com",
pubkey=(uuid4().hex * 2)[:64],
)
result = await update_user_account(updated)
stored = await get_account(account.id)
assert result.id == account.id
assert stored is not None
assert stored.username == updated.username
assert stored.email == updated.email
assert stored.pubkey == updated.pubkey
assert stored.password_hash == account.password_hash
@pytest.mark.anyio
async def test_update_user_extensions_toggles_existing_and_creates_missing(
settings: Settings,
):
original_default_exts = list(settings.lnbits_user_default_extensions)
try:
settings.lnbits_user_default_extensions = []
user = await create_user_account(_account())
finally:
settings.lnbits_user_default_extensions = original_default_exts
await create_user_extension(
UserExtension(user=user.id, extension="keep", active=True)
)
await create_user_extension(
UserExtension(user=user.id, extension="enable", active=False)
)
await create_user_extension(
UserExtension(user=user.id, extension="disable", active=True)
)
await update_user_extensions(user.id, ["keep", "enable", "new-ext"])
user_extensions = {
ext.extension: ext.active for ext in await get_user_extensions(user.id)
}
assert user_extensions == {
"keep": True,
"enable": True,
"disable": False,
"new-ext": True,
}
@pytest.mark.anyio
async def test_check_admin_settings_initializes_cache_and_marks_first_install(
settings: Settings, tmp_path
):
previous_settings = await get_super_settings()
previous_super_user = settings.super_user
previous_data_folder = settings.lnbits_data_folder
previous_admin_ui = settings.lnbits_admin_ui
previous_first_install = settings.first_install
super_user = uuid4().hex
try:
await delete_admin_settings()
settings.super_user = super_user
settings.lnbits_data_folder = str(tmp_path)
settings.lnbits_admin_ui = True
settings.first_install = False
await check_admin_settings()
stored_settings = await get_super_settings()
stored_account = await get_account(super_user)
assert stored_settings is not None
assert stored_settings.super_user == super_user
assert stored_account is not None
assert stored_account.extra.provider == "env"
assert settings.first_install is True
assert (tmp_path / ".super_user").read_text() == super_user
finally:
await delete_admin_settings()
if previous_settings:
await create_admin_settings(
previous_settings.super_user,
previous_settings.dict(exclude={"super_user"}),
)
update_cached_settings(previous_settings.dict())
settings.super_user = previous_super_user
settings.lnbits_data_folder = previous_data_folder
settings.lnbits_admin_ui = previous_admin_ui
settings.first_install = previous_first_install
@pytest.mark.anyio
async def test_init_admin_settings_creates_account_and_wallet_when_missing():
super_user = uuid4().hex
result = await init_admin_settings(super_user)
wallets = await get_wallets(super_user)
assert result.super_user == super_user
assert await get_account(super_user) is not None
assert len(wallets) == 1
@pytest.mark.anyio
async def test_check_register_activation_settings_handles_invitation_codes(
settings: Settings,
):
reusable = "reusable-code"
one_time = "one-time-code"
original_require_activation = settings.lnbits_require_user_activation
original_by_invite = settings.lnbits_user_activation_by_invitation_code
original_reusable = settings.lnbits_register_reusable_activation_code
original_one_time = list(settings.lnbits_register_one_time_activation_codes)
previous_stored_codes = await get_settings_field(
"lnbits_register_one_time_activation_codes"
)
try:
settings.lnbits_require_user_activation = False
assert (
await check_register_activation_settings(
RegisterUser(
username=f"user_{uuid4().hex[:8]}",
password="secret1234",
password_repeat="secret1234",
)
)
is None
)
settings.lnbits_require_user_activation = True
settings.lnbits_user_activation_by_invitation_code = True
settings.lnbits_register_reusable_activation_code = reusable
settings.lnbits_register_one_time_activation_codes = [one_time]
with pytest.raises(ValueError, match="Invitation code cannot be empty."):
await check_register_activation_settings(
RegisterUser(
username=f"user_{uuid4().hex[:8]}",
password="secret1234",
password_repeat="secret1234",
invitation_code=" ",
)
)
assert (
await check_register_activation_settings(
RegisterUser(
username=f"user_{uuid4().hex[:8]}",
password="secret1234",
password_repeat="secret1234",
invitation_code=reusable,
)
)
is None
)
assert (
await check_register_activation_settings(
RegisterUser(
username=f"user_{uuid4().hex[:8]}",
password="secret1234",
password_repeat="secret1234",
invitation_code=one_time,
)
)
is None
)
assert one_time not in settings.lnbits_register_one_time_activation_codes
stored_codes = await get_settings_field(
"lnbits_register_one_time_activation_codes"
)
assert stored_codes is not None
assert stored_codes.value == []
with pytest.raises(ValueError, match="Invalid invitation code."):
await check_register_activation_settings(
RegisterUser(
username=f"user_{uuid4().hex[:8]}",
password="secret1234",
password_repeat="secret1234",
invitation_code="bad-code",
)
)
settings.lnbits_user_activation_by_invitation_code = False
with pytest.raises(ValueError, match="No activation method provided."):
await check_register_activation_settings(
RegisterUser(
username=f"user_{uuid4().hex[:8]}",
password="secret1234",
password_repeat="secret1234",
invitation_code=reusable,
)
)
finally:
settings.lnbits_require_user_activation = original_require_activation
settings.lnbits_user_activation_by_invitation_code = original_by_invite
settings.lnbits_register_reusable_activation_code = original_reusable
settings.lnbits_register_one_time_activation_codes = original_one_time
await set_settings_field(
"lnbits_register_one_time_activation_codes",
previous_stored_codes.value if previous_stored_codes else original_one_time,
)
def _pubkey(value: int) -> str:
return f"{value:064x}"
def _account(
*,
id_: str | None = None,
username: str | None = None,
email: str | None = None,
pubkey: str | None = None,
) -> Account:
account_id = id_ or uuid4().hex
return Account(
id=account_id,
username=username or f"user_{account_id[:8]}",
email=email or f"{account_id[:8]}@example.com",
pubkey=pubkey,
)