fix: api tests

This commit is contained in:
Vlad Stan
2024-10-10 09:08:31 +02:00
committed by dni ⚡
parent b0e2861aef
commit 8725e1a2b7
8 changed files with 98 additions and 107 deletions
+1
View File
@@ -176,6 +176,7 @@ async def get_user(
id=account.id,
email=account.email,
username=account.username,
pubkey=account.pubkey,
extra=account.extra,
created_at=account.created_at,
updated_at=account.updated_at,
+4 -1
View File
@@ -54,6 +54,7 @@ from .crud import (
create_wallet,
get_account,
get_account_by_email,
get_account_by_pubkey,
get_account_by_username,
get_payments,
get_standalone_payment,
@@ -849,8 +850,10 @@ async def create_user_account(
if account.email and await get_account_by_email(account.email):
raise ValueError("Email already exists.")
if account.pubkey and await get_account_by_pubkey(account.pubkey):
raise ValueError("Pubkey already exists.")
if account.id:
print("### account", account)
user_uuid4 = UUID(hex=account.id, version=4)
assert user_uuid4.hex == account.id, "User ID is not valid UUID4 hex string"
else:
+3 -10
View File
@@ -14,7 +14,6 @@ from fastapi import (
from fastapi.exceptions import HTTPException
from fastapi.responses import StreamingResponse
from lnbits.core.crud import create_account, create_wallet
from lnbits.core.models import (
BaseWallet,
ConversionData,
@@ -38,7 +37,7 @@ from lnbits.utils.exchange_rates import (
satoshis_amount_as_fiat,
)
from ..services import perform_lnurlauth
from ..services import create_user_account, perform_lnurlauth
api_router = APIRouter(tags=["Core"])
@@ -63,14 +62,8 @@ async def api_wallets(user: User = Depends(check_user_exists)) -> list[BaseWalle
@api_router.post("/api/v1/account", response_model=Wallet)
async def api_create_account(data: CreateWallet) -> Wallet:
if not settings.new_accounts_allowed:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Account creation is disabled.",
)
account = await create_account()
wallet = await create_wallet(user_id=account.id, wallet_name=data.name)
return wallet
user = await create_user_account(wallet_name=data.name)
return user.wallets[0]
@api_router.get("/api/v1/lnurlscan/{code}")
+35 -54
View File
@@ -11,6 +11,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID, SSOBase
from loguru import logger
from lnbits.core.services import create_user_account
from lnbits.decorators import access_token_payload, check_user_exists
from lnbits.helpers import (
create_access_token,
@@ -18,13 +19,11 @@ from lnbits.helpers import (
encrypt_internal_message,
is_valid_email_address,
is_valid_username,
urlsafe_short_hash,
)
from lnbits.settings import AuthMethods, settings
from lnbits.utils.nostr import normalize_public_key, verify_event
from ..crud import (
create_account,
get_account,
get_account_by_email,
get_account_by_pubkey,
@@ -84,7 +83,7 @@ async def nostr_login(request: Request) -> JSONResponse:
pubkey=event["pubkey"],
extra=UserExtra(provider="nostr"),
)
await create_account(account)
await create_user_account(account)
return _auth_success_response(account.username or "", account.id, account.email)
@@ -182,12 +181,12 @@ async def register(data: CreateUser) -> JSONResponse:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid email.")
account = Account(
id=urlsafe_short_hash(),
id=uuid4().hex,
email=data.email,
username=data.username,
)
account.hash_password(data.password)
await create_account(account)
await create_user_account(account)
return _auth_success_response(account.username, account.id, account.email)
@@ -225,36 +224,22 @@ async def update_password(
payload: AccessTokenPayload = Depends(access_token_payload),
) -> Optional[User]:
if data.user_id != user.id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user ID."
)
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
_validate_auth_timeout(payload.auth_time)
if (
data.username
and user.username != data.username
and await get_account_by_username(data.username)
):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists."
)
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
account = await get_account(user.id)
if not account:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Account not found."
)
assert account, "Account not found."
# old accounts do not have a password
if account.password_hash:
if not data.password_old:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Missing old password."
)
if not account.verify_password(data.password_old):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid credentials."
)
_validate_auth_timeout(payload.auth_time)
assert data.password_old, "Missing old password."
assert account.verify_password(data.password_old), "Invalid old password."
account.username = data.username
account.hash_password(data.password)
@@ -275,9 +260,12 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
assert data.password == data.password_repeat, "Passwords do not match."
assert data.reset_key[:10].startswith("reset_key_"), "This is not a reset key."
reset_data_json = decrypt_internal_message(
base64.b64decode(data.reset_key[10:]).decode()
)
try:
reset_key = base64.b64decode(data.reset_key[10:]).decode()
reset_data_json = decrypt_internal_message(reset_key)
except Exception as exc:
raise ValueError("Invalid reset key.") from exc
assert reset_data_json, "Cannot process reset key."
action, user_id, request_time = json.loads(reset_data_json)
@@ -384,12 +372,10 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
account.extra.email_verified = True
await update_account(account)
else:
if not settings.new_accounts_allowed:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Account creation is disabled.")
account = Account(
id=uuid4().hex, email=email, extra=UserExtra(email_verified=True)
)
await create_account(account)
await create_user_account(account)
return _auth_redirect_response(redirect_path, email)
@@ -476,40 +462,35 @@ def _nostr_nip98_event(request: Request) -> dict:
event = json.loads(event_json)
except Exception as exc:
logger.warning(exc)
if not event:
raise HTTPException(
HTTPStatus.BAD_REQUEST, "Nostr login event cannot be parsed."
)
assert event, "Nostr login event cannot be parsed."
if not verify_event(event):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr login event is not valid.")
if event["kind"] != 27_235:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid event kind.")
assert event["kind"] == 27_235, "Invalid event kind."
auth_threshold = settings.auth_credetials_update_threshold
if abs(time() - event["created_at"]) > auth_threshold:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
f"{auth_threshold} seconds have passed since the event was signed.",
)
assert (
abs(time() - event["created_at"]) < auth_threshold
), f"More than {auth_threshold} seconds have passed since the event was signed."
method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None)
if not method:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Tag 'method' is missing.")
if method.upper() != "POST":
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid value for tag 'method'.")
assert method, "Tag 'method' is missing."
assert method.upper() == "POST", "Invalid value for tag 'method'."
url = next((v for k, v in event["tags"] if k == "u"), None)
if not url:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Tag 'u' for URL is missing.")
assert url, "Tag 'u' for URL is missing."
accepted_urls = [f"{u}/nostr" for u in settings.nostr_absolute_request_urls]
if url not in accepted_urls:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid value for tag 'u'.")
assert url in accepted_urls, f"Invalid value for tag 'u': '{url}'."
return event
def _validate_auth_timeout(auth_time: Optional[int] = None):
if int(time()) - int(auth_time or 0) > settings.auth_credetials_update_threshold:
def _validate_auth_timeout(auth_time: Optional[int] = 0):
if abs(time() - (auth_time or 0)) > settings.auth_credetials_update_threshold:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"You can only update your credentials in the first"
f" {settings.auth_credetials_update_threshold} seconds after login."
" Please login again!",
f" {settings.auth_credetials_update_threshold} seconds."
" Please login again or ask a new reset key!",
)
+2 -3
View File
@@ -14,7 +14,7 @@ from pydantic.types import UUID4
from lnbits.core.extensions.models import Extension, ExtensionMeta, InstallableExtension
from lnbits.core.helpers import to_valid_user_id
from lnbits.core.models import User
from lnbits.core.services import create_invoice
from lnbits.core.services import create_invoice, create_user_account
from lnbits.decorators import check_admin, check_user_exists
from lnbits.helpers import template_renderer
from lnbits.settings import settings
@@ -22,7 +22,6 @@ from lnbits.wallets import get_funding_source
from ...utils.exchange_rates import allowed_currencies, currencies
from ..crud import (
create_account,
create_wallet,
get_dbversions,
get_installed_extensions,
@@ -424,7 +423,7 @@ async def lnurlwallet(request: Request):
status_code=HTTPStatus.BAD_REQUEST,
detail="Invalid lnurl. Expected maxWithdrawable",
)
account = await create_account()
account = await create_user_account()
wallet = await create_wallet(user_id=account.id)
_, payment_request = await create_invoice(
wallet_id=wallet.id,
+8
View File
@@ -76,6 +76,14 @@ def register_exception_handlers(app: FastAPI):
content={"detail": str(exc)},
)
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
logger.warning(f"ValueError: {exc!s}")
return render_html_error(request, exc) or JSONResponse(
status_code=HTTPStatus.BAD_REQUEST,
content={"detail": str(exc)},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
+28 -26
View File
@@ -255,7 +255,8 @@ async def test_register_email_twice(http_client: AsyncClient):
"email": f"u21.{tiny_id}@lnbits.com",
},
)
assert response.status_code == 403, "Not allowed."
assert response.status_code == 400, "Not allowed."
assert response.json().get("detail") == "Email already exists."
@@ -285,7 +286,7 @@ async def test_register_username_twice(http_client: AsyncClient):
"email": f"u21.{tiny_id_2}@lnbits.com",
},
)
assert response.status_code == 403, "Not allowed."
assert response.status_code == 400, "Not allowed."
assert response.json().get("detail") == "Username already exists."
@@ -414,8 +415,8 @@ async def test_alan_change_password_old_nok(user_alan: User, http_client: AsyncC
},
)
assert response.status_code == 403, "Old password bad."
assert response.json().get("detail") == "Invalid credentials."
assert response.status_code == 400, "Old password bad."
assert response.json().get("detail") == "Invalid old password."
@pytest.mark.asyncio
@@ -469,7 +470,7 @@ async def test_alan_change_password_auth_threshold_expired(
},
)
assert response.status_code == 403, "Treshold expired."
assert response.status_code == 400
assert (
response.json().get("detail") == "You can only update your credentials"
" in the first 1 seconds."
@@ -507,6 +508,7 @@ async def test_register_nostr_ok(http_client: AsyncClient):
response = await http_client.get(
"/api/v1/auth", headers={"Authorization": f"Bearer {access_token}"}
)
user = User(**response.json())
assert user.username is None, "No username."
assert user.email is None, "No email."
@@ -547,13 +549,13 @@ async def test_register_nostr_bad_header(http_client: AsyncClient):
)
assert response.status_code == 401, "Non nostr header."
assert response.json().get("detail") == "Authorization header is not nostr."
assert response.json().get("detail") == "Invalid Authorization scheme."
response = await http_client.post(
"/api/v1/auth/nostr",
headers={"Authorization": "nostr xyz"},
)
assert response.status_code == 401, "Nostr not base64."
assert response.status_code == 400, "Nostr not base64."
assert response.json().get("detail") == "Nostr login event cannot be parsed."
@@ -565,7 +567,7 @@ async def test_register_nostr_bad_event(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event}"},
)
assert response.status_code == 401, "Nostr event expired."
assert response.status_code == 400, "Nostr event expired."
assert (
response.json().get("detail")
== f"More than {settings.auth_credetials_update_threshold}"
@@ -581,7 +583,7 @@ async def test_register_nostr_bad_event(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event}"},
)
assert response.status_code == 401, "Nostr event signature invalid."
assert response.status_code == 400, "Nostr event signature invalid."
assert response.json().get("detail") == "Nostr login event is not valid."
@@ -598,7 +600,7 @@ async def test_register_nostr_bad_event_kind(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event_bad_kind}"},
)
assert response.status_code == 401, "Nostr event kind invalid."
assert response.status_code == 400, "Nostr event kind invalid."
assert response.json().get("detail") == "Invalid event kind."
@@ -617,7 +619,7 @@ async def test_register_nostr_bad_event_tag_u(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event_tag_kind}"},
)
assert response.status_code == 401, "Nostr event tag missing."
assert response.status_code == 400, "Nostr event tag missing."
assert response.json().get("detail") == "Tag 'method' is missing."
event_bad_kind["tags"] = [["u", "http://localhost:5000/nostr"], ["method", "XYZ"]]
@@ -630,8 +632,8 @@ async def test_register_nostr_bad_event_tag_u(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event_tag_kind}"},
)
assert response.status_code == 401, "Nostr event tag invalid."
assert response.json().get("detail") == "Incorrect value for tag 'method'."
assert response.status_code == 400, "Nostr event tag invalid."
assert response.json().get("detail") == "Invalid value for tag 'method'."
@pytest.mark.asyncio
@@ -649,7 +651,7 @@ async def test_register_nostr_bad_event_tag_menthod(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event}"},
)
assert response.status_code == 401, "Nostr event tag missing."
assert response.status_code == 400, "Nostr event tag missing."
assert response.json().get("detail") == "Tag 'u' for URL is missing."
event_bad_kind["tags"] = [["u", "http://demo.lnbits.com/nostr"], ["method", "POST"]]
@@ -662,15 +664,15 @@ async def test_register_nostr_bad_event_tag_menthod(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event}"},
)
assert response.status_code == 401, "Nostr event tag invalid."
assert response.status_code == 400, "Nostr event tag invalid."
assert (
response.json().get("detail") == "Incorrect value for tag 'u':"
response.json().get("detail") == "Invalid value for tag 'u':"
" 'http://demo.lnbits.com/nostr'."
)
################################ CHANGE PUBLIC KEY ################################
async def test_change_pubkey_npub_ok(http_client: AsyncClient, user_alan: User):
async def test_change_pubkey_npub_ok(http_client: AsyncClient):
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
@@ -790,7 +792,7 @@ async def test_change_pubkey_ok(http_client: AsyncClient, user_alan: User):
},
)
assert response.status_code == 403, "Pubkey already used."
assert response.status_code == 400, "Pubkey already used."
assert response.json().get("detail") == "Public key already in use."
@@ -852,11 +854,11 @@ async def test_alan_change_pubkey_auth_threshold_expired(
},
)
assert response.status_code == 403, "Treshold expired."
assert response.status_code == 400, "Treshold expired."
assert (
response.json().get("detail") == "You can only update your credentials"
" in the first 1 seconds after login."
" Please login again!"
" in the first 1 seconds."
" Please login again or ask a new reset key!"
)
@@ -929,7 +931,7 @@ async def test_request_reset_key_user_not_found(http_client: AsyncClient):
},
)
assert response.status_code == 403, "User does not exist."
assert response.status_code == 404, "User does not exist."
assert response.json().get("detail") == "User not found."
@@ -975,7 +977,7 @@ async def test_reset_username_passwords_do_not_matcj(
},
)
assert response.status_code == 403, "Passwords do not match."
assert response.status_code == 400, "Passwords do not match."
assert response.json().get("detail") == "Passwords do not match."
@@ -990,8 +992,8 @@ async def test_reset_username_password_bad_key(http_client: AsyncClient):
"password_repeat": "secret0000",
},
)
assert response.status_code == 500, "Bad reset key."
assert response.json().get("detail") == "Cannot reset user password."
assert response.status_code == 400, "Bad reset key."
assert response.json().get("detail") == "Invalid reset key."
@pytest.mark.asyncio
@@ -1013,7 +1015,7 @@ async def test_reset_password_auth_threshold_expired(
},
)
assert response.status_code == 403, "Treshold expired."
assert response.status_code == 400, "Treshold expired."
assert (
response.json().get("detail") == "You can only update your credentials"
" in the first 1 seconds."
+17 -13
View File
@@ -16,15 +16,15 @@ from httpx import ASGITransport, AsyncClient
from lnbits.app import create_app
from lnbits.core.crud import (
create_account,
create_wallet,
delete_account,
get_account,
get_account_by_username,
get_user,
update_payment_status,
)
from lnbits.core.models import Account, CreateInvoice, PaymentState
from lnbits.core.services import update_wallet_balance
from lnbits.core.services import create_user_account, update_wallet_balance
from lnbits.core.views.payment_api import api_payments_create_invoice
from lnbits.db import DB_TYPE, SQLITE, Database
from lnbits.settings import AuthMethods, settings
@@ -46,6 +46,7 @@ def run_before_and_after_tests():
##### BEFORE TEST RUN #####
settings.lnbits_allow_new_accounts = True
settings.lnbits_allowed_users = []
settings.auth_allowed_methods = AuthMethods.all()
settings.auth_credetials_update_threshold = 120
settings.lnbits_reserve_fee_percent = 1
@@ -103,20 +104,23 @@ async def db():
@pytest_asyncio.fixture(scope="session")
async def user_alan():
account = await get_account_by_username("alan")
if not account:
account = Account(
id=uuid4().hex,
email="alan@lnbits.com",
username="alan",
)
account.hash_password("secret1234")
account = await create_account(account)
yield account
if account:
await delete_account(account.id)
account = Account(
id=uuid4().hex,
email="alan@lnbits.com",
username="alan",
)
account.hash_password("secret1234")
user = await create_user_account(account)
yield user
@pytest_asyncio.fixture(scope="session")
async def from_user():
user = await create_account()
user = await create_user_account()
yield user
@@ -141,7 +145,7 @@ async def from_wallet_ws(from_wallet, test_client):
@pytest_asyncio.fixture(scope="session")
async def to_user():
user = await create_account()
user = await create_user_account()
yield user