408 lines
13 KiB
Python
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,
|
|
)
|