282 lines
8.9 KiB
Python
282 lines
8.9 KiB
Python
import hashlib
|
|
|
|
import jwt
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
|
|
from lnbits.helpers import (
|
|
camel_to_snake,
|
|
camel_to_words,
|
|
check_callback_url,
|
|
create_access_token,
|
|
decrypt_internal_message,
|
|
download_url,
|
|
encrypt_internal_message,
|
|
file_hash,
|
|
filter_dict_keys,
|
|
get_api_routes,
|
|
get_db_vendor_name,
|
|
is_camel_case,
|
|
is_lnbits_version_ok,
|
|
is_snake_case,
|
|
is_valid_email_address,
|
|
is_valid_external_id,
|
|
is_valid_label,
|
|
is_valid_pubkey,
|
|
is_valid_username,
|
|
lowercase_first_letter,
|
|
normalize_endpoint,
|
|
normalize_path,
|
|
path_segments,
|
|
sha256s,
|
|
snake_to_camel,
|
|
static_url_for,
|
|
url_for,
|
|
version_parse,
|
|
)
|
|
from lnbits.settings import Settings
|
|
|
|
|
|
@pytest.mark.anyio
|
|
def test_check_callback_url_not_allowed(settings: Settings):
|
|
settings.lnbits_callback_url_rules = [
|
|
"https?://([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(:\\d+)?"
|
|
]
|
|
with pytest.raises(ValueError, match="Callback not allowed. URL: xx. Netloc: ."):
|
|
check_callback_url("xx")
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Callback not allowed. URL: http://localhost:3000/callback. "
|
|
"Netloc: localhost:3000. Please check your admin settings.",
|
|
):
|
|
check_callback_url("http://localhost:3000/callback")
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Callback not allowed. URL: https://localhost:3000/callback. "
|
|
"Netloc: localhost:3000. Please check your admin settings.",
|
|
):
|
|
check_callback_url("https://localhost:3000/callback")
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Callback not allowed. URL: http://192.168.2.2:3000/callback. "
|
|
"Netloc: 192.168.2.2:3000. Please check your admin settings.",
|
|
):
|
|
check_callback_url("http://192.168.2.2:3000/callback")
|
|
|
|
|
|
@pytest.mark.anyio
|
|
def test_check_callback_url_no_rules(settings: Settings):
|
|
settings.lnbits_callback_url_rules = [
|
|
"https?://([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(:\\d+)?"
|
|
]
|
|
settings.lnbits_callback_url_rules.append(".*")
|
|
check_callback_url("xyz")
|
|
|
|
|
|
@pytest.mark.anyio
|
|
def test_check_callback_url_allow_all(settings: Settings):
|
|
settings.lnbits_callback_url_rules = []
|
|
check_callback_url("xyz")
|
|
|
|
|
|
@pytest.mark.anyio
|
|
def test_check_callback_url_allowed(settings: Settings):
|
|
settings.lnbits_callback_url_rules = [
|
|
"https?://([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(:\\d+)?"
|
|
]
|
|
check_callback_url("http://google.com/callback")
|
|
check_callback_url("http://google.com:80/callback")
|
|
check_callback_url("http://google.com:8080/callback")
|
|
check_callback_url("https://google.com/callback")
|
|
check_callback_url("https://google.com:443/callback")
|
|
|
|
|
|
@pytest.mark.anyio
|
|
def test_check_callback_url_multiple_rules(settings: Settings):
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Callback not allowed. URL: http://localhost:3000/callback. "
|
|
"Netloc: localhost:3000. Please check your admin settings.",
|
|
):
|
|
check_callback_url("http://localhost:3000/callback")
|
|
|
|
settings.lnbits_callback_url_rules.append("http://localhost:3000")
|
|
check_callback_url("http://localhost:3000/callback") # should not raise
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Callback not allowed. URL: https://localhost:3000/callback. "
|
|
"Netloc: localhost:3000. Please check your admin settings.",
|
|
):
|
|
check_callback_url("https://localhost:3000/callback")
|
|
|
|
settings.lnbits_callback_url_rules.append("https://localhost:3000")
|
|
check_callback_url("https://localhost:3000/callback") # should not raise
|
|
|
|
|
|
def test_get_db_vendor_name(settings: Settings):
|
|
original_database_url = settings.lnbits_database_url
|
|
try:
|
|
settings.lnbits_database_url = None
|
|
assert get_db_vendor_name() == "SQLite"
|
|
|
|
settings.lnbits_database_url = "postgres://localhost/db"
|
|
assert get_db_vendor_name() == "PostgreSQL"
|
|
|
|
settings.lnbits_database_url = "cockroachdb://localhost/db"
|
|
assert get_db_vendor_name() == "CockroachDB"
|
|
finally:
|
|
settings.lnbits_database_url = original_database_url
|
|
|
|
|
|
def test_url_helpers(settings: Settings):
|
|
assert url_for("/api/v1/wallet", external=False, usr="user") == (
|
|
"/api/v1/wallet?usr=user&"
|
|
)
|
|
assert url_for("/api/v1/wallet", external=True, usr="user") == (
|
|
f"http://{settings.host}:{settings.port}/api/v1/wallet?usr=user&"
|
|
)
|
|
assert static_url_for("static", "bundle.min.js") == (
|
|
f"/static/bundle.min.js?v={settings.server_startup_time}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "validator"),
|
|
[
|
|
("alice@example.com", is_valid_email_address),
|
|
("alice_1", is_valid_username),
|
|
("Label 1", is_valid_label),
|
|
("external-id-1", is_valid_external_id),
|
|
("a" * 64, is_valid_pubkey),
|
|
],
|
|
)
|
|
def test_validation_helpers_valid(value, validator):
|
|
assert validator(value) is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "validator"),
|
|
[
|
|
("alice@example", is_valid_email_address),
|
|
("_alice", is_valid_username),
|
|
("bad/label", is_valid_label),
|
|
("contains spaces", is_valid_external_id),
|
|
("xyz", is_valid_pubkey),
|
|
],
|
|
)
|
|
def test_validation_helpers_invalid(value, validator):
|
|
assert validator(value) is False
|
|
|
|
|
|
def test_is_valid_external_id_rejects_long_and_multiline_values():
|
|
assert is_valid_external_id("x" * 257) is False
|
|
assert is_valid_external_id("evil\nnewline") is False
|
|
|
|
|
|
def test_access_token_and_internal_message_helpers(settings: Settings):
|
|
token = create_access_token(
|
|
{"sub": "alice", "usr": None, "email": "alice@example.com"},
|
|
token_expire_minutes=1,
|
|
)
|
|
payload = jwt.decode(token, settings.auth_secret_key, ["HS256"])
|
|
assert payload["sub"] == "alice"
|
|
assert payload["email"] == "alice@example.com"
|
|
assert "usr" not in payload
|
|
assert "exp" in payload
|
|
|
|
assert encrypt_internal_message(None) is None
|
|
assert decrypt_internal_message(None) is None
|
|
|
|
encrypted = encrypt_internal_message("secret-message", urlsafe=True)
|
|
assert encrypted is not None
|
|
assert decrypt_internal_message(encrypted, urlsafe=True) == "secret-message"
|
|
|
|
|
|
def test_filter_dict_keys_returns_copy_when_no_filters():
|
|
original = {"a": 1, "b": 2}
|
|
|
|
clone = filter_dict_keys(original, None)
|
|
filtered = filter_dict_keys(original, ["b", "missing"])
|
|
|
|
assert clone == original
|
|
assert clone is not original
|
|
assert filtered == {"b": 2}
|
|
|
|
|
|
def test_version_helpers(settings: Settings):
|
|
original_version = settings.version
|
|
try:
|
|
settings.version = "1.2.3"
|
|
assert version_parse("1.2.3rc4") == version_parse("1.2.3")
|
|
assert version_parse("invalid-version") == version_parse("0.0.0")
|
|
assert is_lnbits_version_ok("1.2.0", "2.0.0") is True
|
|
assert is_lnbits_version_ok("2.0.0", None) is False
|
|
assert is_lnbits_version_ok(None, "1.2.3") is False
|
|
finally:
|
|
settings.version = original_version
|
|
|
|
|
|
def test_download_url_rejects_non_http_schemes(tmp_path):
|
|
with pytest.raises(
|
|
ValueError, match="Invalid URL: ftp://example.com. Must start with 'http'"
|
|
):
|
|
download_url("ftp://example.com", tmp_path / "download.bin")
|
|
|
|
|
|
def test_file_hash(tmp_path):
|
|
filename = tmp_path / "payload.txt"
|
|
filename.write_text("hello world")
|
|
|
|
assert file_hash(filename) == hashlib.sha256(b"hello world").hexdigest()
|
|
|
|
|
|
def test_get_api_routes_extracts_v1_paths():
|
|
app = FastAPI()
|
|
|
|
@app.get("/api/v1/payments")
|
|
async def payments():
|
|
return {}
|
|
|
|
@app.get("/myext/api/v1/settings")
|
|
async def extension_settings():
|
|
return {}
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {}
|
|
|
|
routes = get_api_routes([*app.routes, object()])
|
|
|
|
assert routes == {
|
|
"/api/v1/payments": "Payments",
|
|
"/myext/api/v1": "Myext",
|
|
}
|
|
|
|
|
|
def test_path_and_case_helpers():
|
|
assert path_segments("/wallet/path") == ["wallet", "path"]
|
|
assert path_segments("/upgrades/ext/assets/app.js") == ["assets", "app.js"]
|
|
assert normalize_path(None) == "/"
|
|
assert normalize_path("/upgrades/ext/assets/app.js") == "/assets/app.js"
|
|
assert normalize_endpoint("example.com/") == "https://example.com"
|
|
assert normalize_endpoint("ws://socket.example.com") == "ws://socket.example.com"
|
|
assert (
|
|
normalize_endpoint("http://example.com/", add_proto=False)
|
|
== "http://example.com"
|
|
)
|
|
|
|
assert camel_to_words("CamelCaseName") == "Camel Case Name"
|
|
assert camel_to_snake("CamelCaseName") == "camel_case_name"
|
|
assert snake_to_camel("snake_case_name") == "snakeCaseName"
|
|
assert snake_to_camel("snake_case_name", capitalize_first=True) == "SnakeCaseName"
|
|
assert is_camel_case("CamelCase1") is True
|
|
assert is_camel_case("camelCase") is False
|
|
assert is_snake_case("snake_case_1") is True
|
|
assert is_snake_case("SnakeCase") is False
|
|
assert lowercase_first_letter("Hello") == "hello"
|
|
assert sha256s("hello") == hashlib.sha256(b"hello").hexdigest()
|