test: add tests
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
]
|
||||
@@ -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
|
||||
@@ -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}}$")
|
||||
Reference in New Issue
Block a user