test: add tests

This commit is contained in:
Vlad Stan
2026-07-07 18:32:54 +03:00
parent 15d26093a5
commit 4f072a6247
12 changed files with 543 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
+149
View File
@@ -0,0 +1,149 @@
from __future__ import annotations
import os
import shutil
import socket
import subprocess
import sys
import time
from collections.abc import Iterator
from pathlib import Path
from typing import Any
import httpx
import pytest
from tests.wasm_ext.helpers import EXTENSION_ID, SERVER_HOST, LiveLNbitsServer
REPO_ROOT = Path(__file__).resolve().parents[2]
@pytest.fixture(scope="session")
def browser_name() -> str:
return "chromium"
@pytest.fixture(scope="session")
def lnbits_server(
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[LiveLNbitsServer]:
run_root = tmp_path_factory.mktemp("wasm_ext_e2e")
data_dir = run_root / "data"
extensions_root = run_root / "extensions-root"
extension_target = extensions_root / "extensions" / EXTENSION_ID
fixture_extension = REPO_ROOT / "tests" / "fixtures" / EXTENSION_ID
shutil.copytree(fixture_extension, extension_target)
port = _free_tcp_port()
base_url = f"http://{SERVER_HOST}:{port}"
env = {
**os.environ,
"AUTH_HTTPS_ONLY": "false",
"DEBUG": "true",
"HOST": SERVER_HOST,
"LNBITS_ADMIN_UI": "true",
"LNBITS_BACKEND_WALLET_CLASS": "FakeWallet",
"LNBITS_DATA_FOLDER": str(data_dir),
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": "false",
"LNBITS_EXTENSIONS_PATH": str(extensions_root),
"LNBITS_PATH": str(REPO_ROOT),
"PORT": str(port),
"PYTHONUNBUFFERED": "1",
}
process = subprocess.Popen( # noqa: S603
[
sys.executable,
"-m",
"uvicorn",
"lnbits.__main__:app",
"--host",
SERVER_HOST,
"--port",
str(port),
"--loop",
"asyncio",
"--log-level",
"warning",
],
cwd=REPO_ROOT,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
try:
_wait_for_first_install_page(process, base_url)
auth_cookies = _complete_first_install(base_url)
yield LiveLNbitsServer(base_url=base_url, auth_cookies=auth_cookies)
finally:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=10)
@pytest.fixture
def authenticated_page(page: Any, lnbits_server: LiveLNbitsServer) -> Any:
page.context.add_cookies(lnbits_server.auth_cookies)
return page
def _free_tcp_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind((SERVER_HOST, 0))
return int(sock.getsockname()[1])
def _wait_for_first_install_page(
process: subprocess.Popen[str],
base_url: str,
) -> None:
deadline = time.monotonic() + 60
last_error: Exception | None = None
with httpx.Client(follow_redirects=False, timeout=2) as client:
while time.monotonic() < deadline:
if process.poll() is not None:
output = process.stdout.read() if process.stdout else ""
raise RuntimeError(f"LNbits exited before startup:\n{output}")
try:
response = client.get(f"{base_url}/first_install")
if response.status_code == 200:
return
except httpx.HTTPError as exc:
last_error = exc
time.sleep(0.25)
output = process.stdout.read() if process.stdout else ""
raise TimeoutError(f"LNbits did not start: {last_error}\n{output}")
def _complete_first_install(base_url: str) -> list[dict[str, Any]]:
with httpx.Client(base_url=base_url, follow_redirects=False, timeout=10) as client:
response = client.put(
"/api/v1/auth/first_install",
json={
"username": "wasmtest-admin",
"password": "secret1234",
"password_repeat": "secret1234",
},
)
response.raise_for_status()
enable_response = client.put(f"/api/v1/extension/{EXTENSION_ID}/enable")
enable_response.raise_for_status()
return [
{
"name": cookie.name,
"value": cookie.value,
"url": base_url,
"secure": cookie.secure,
"sameSite": "Lax",
}
for cookie in client.cookies.jar
]
+14
View File
@@ -0,0 +1,14 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
EXTENSION_ID = "lnbits-wasm-test-extension"
SERVER_HOST = "127.0.0.1"
@dataclass(frozen=True)
class LiveLNbitsServer:
base_url: str
auth_cookies: list[dict[str, Any]]
extension_id: str = EXTENSION_ID
+94
View File
@@ -0,0 +1,94 @@
from __future__ import annotations
import re
from re import Pattern
from playwright.sync_api import Page, expect
from tests.wasm_ext.helpers import LiveLNbitsServer
def test_public_wasm_page_loads_sandboxed_frame(
page: Page,
lnbits_server: LiveLNbitsServer,
) -> None:
public_url = (
f"{lnbits_server.base_url}/ext/{lnbits_server.extension_id}/public/item-123"
"?source=test"
)
frame_url_part = f"/ext-frame/{lnbits_server.extension_id}/1"
with page.expect_response(lambda response: frame_url_part in response.url) as info:
page.goto(public_url)
frame_response = info.value
assert frame_response.status == 200
assert "sandbox allow-scripts" in frame_response.headers["content-security-policy"]
assert frame_response.headers["cache-control"] == "no-store"
assert frame_response.headers["x-content-type-options"] == "nosniff"
frame = page.locator("iframe.wasm-extension-frame")
expect(frame).to_have_attribute("sandbox", "allow-scripts")
expect(frame).to_have_attribute("allow", "clipboard-write")
expect(frame).to_have_attribute("referrerpolicy", "no-referrer")
expect(frame).to_have_attribute("src", _frame_src_pattern(frame_url_part))
expect(
page.frame_locator("iframe.wasm-extension-frame").locator("body")
).to_contain_text("WASM Test Public")
def test_frame_token_is_route_bound_required_and_single_use(
page: Page,
lnbits_server: LiveLNbitsServer,
) -> None:
frame_config = page.request.post(
f"{lnbits_server.base_url}/api/v1/ext/"
f"{lnbits_server.extension_id}/_ui/frame",
data={
"path": f"/ext/{lnbits_server.extension_id}/public/item-123",
"query": {"source": "test"},
},
)
assert frame_config.status == 200
frame_url = frame_config.json()["frameUrl"]
missing_token = page.request.get(
f"{lnbits_server.base_url}/ext-frame/{lnbits_server.extension_id}/1"
)
assert missing_token.status == 404
wrong_route = page.request.get(
f"{lnbits_server.base_url}{frame_url.replace('/1?', '/0?')}"
)
assert wrong_route.status == 404
first_use = page.request.get(f"{lnbits_server.base_url}{frame_url}")
assert first_use.status == 200
assert "form-action 'none'" in first_use.headers["content-security-policy"]
second_use = page.request.get(f"{lnbits_server.base_url}{frame_url}")
assert second_use.status == 404
def test_private_wasm_page_uses_lnbits_shell_and_private_frame(
authenticated_page: Page,
lnbits_server: LiveLNbitsServer,
) -> None:
page = authenticated_page
frame_url_part = f"/ext-frame/{lnbits_server.extension_id}/0"
with page.expect_response(lambda response: frame_url_part in response.url) as info:
page.goto(f"{lnbits_server.base_url}/ext/{lnbits_server.extension_id}")
assert info.value.status == 200
expect(page.locator("body")).to_contain_text("LNbits")
frame = page.locator("iframe.wasm-extension-frame")
expect(frame).to_have_attribute("sandbox", "allow-scripts")
expect(frame).to_have_attribute("src", _frame_src_pattern(frame_url_part))
expect(
page.frame_locator("iframe.wasm-extension-frame").locator("body")
).to_contain_text("WASM Test Admin")
def _frame_src_pattern(frame_url_part: str) -> Pattern[str]:
return re.compile(f"^{re.escape(frame_url_part)}\\?frame_token=[a-f0-9]{{32}}$")