ArchiveBox/archivebox/tests/test_ui_admin_config_widget.py
2026-07-25 14:50:15 -07:00

293 lines
13 KiB
Python

from pathlib import Path
import pytest
from django.contrib.auth.models import AnonymousUser
from django.template.loader import render_to_string
from django.test import RequestFactory
from archivebox.base_models.admin import KeyValueWidget
from archivebox.config.common import get_config
from archivebox.core.setup_wizard import get_base_url_mismatch_context, get_setup_wizard_context
from archivebox.core.templatetags.core_tags import system_warnings_banner
STATIC_DIR = Path(__file__).parents[1] / "templates" / "static"
SETUP_WIZARD_CSS = (STATIC_DIR / "setup_wizard.css").read_text()
SETUP_WIZARD_JS = (STATIC_DIR / "setup_wizard.js").read_text()
def test_key_value_widget_renders_enum_autocomplete_metadata():
html = str(
KeyValueWidget().render(
"config",
{"CHROME_WAIT_FOR": "load"},
attrs={"id": "id_config"},
),
)
assert '"enum": ["domcontentloaded", "load", "networkidle0", "networkidle2"]' in html
assert 'class="kv-value-options"' in html
assert 'class="kv-help"' in html
assert "configureValueInput_id_config" in html
assert "describeMeta_id_config" in html
assert "validateValueAgainstMeta_id_config" in html
def test_key_value_widget_renders_numeric_and_pattern_constraints():
html = str(KeyValueWidget().render("config", {}, attrs={"id": "id_config"}))
assert '"minimum": 0' in html
assert '"pattern": "^\\\\d+,\\\\d+$"' in html
assert "Expected: " in html
assert "Example: " in html
assert "setValueValidationState_id_config" in html
assert "coerceValueForStorage_id_config" in html
def test_key_value_widget_accepts_common_boolean_spellings():
html = str(KeyValueWidget().render("config", {"CHECK_SSL_VALIDITY": "True"}, attrs={"id": "id_config"}))
assert "enumValues = ['True', 'False']" in html
assert "raw.toLowerCase()" in html
assert "lowered === 'true' || raw === '1'" in html
assert "lowered === 'false' || raw === '0'" in html
def test_key_value_widget_shows_array_and_object_examples_and_binary_rules():
html = str(KeyValueWidget().render("config", {"NODE_BINARY": "node"}, attrs={"id": "id_config"}))
assert 'Example: ["--extra-arg"]' in html
assert "Example: wget or /usr/bin/wget" in html
assert "validateBinaryValue_id_config" in html
assert "meta.key.endsWith('_BINARY')" in html
assert "Binary paths cannot contain quotes" in html
def test_key_value_widget_falls_back_to_binary_validation_for_unknown_binary_keys():
html = str(
KeyValueWidget().render(
"config",
{"NODE_BINARY": "/opt/homebrew/bin/node"},
attrs={"id": "id_config"},
),
)
assert "function getMetaForKey_id_config" in html
assert "if (key.endsWith('_BINARY'))" in html
assert "Path to binary executable" in html
def test_key_value_widget_prefills_known_config_values_from_query_string():
html = str(KeyValueWidget().render("config", {}, attrs={"id": "id_config"}))
assert "new URLSearchParams(window.location.search)" in html
assert "prefillConfigFromQuery_id_config" in html
assert "configMeta_id_config[key]" in html
assert "valueInput.value = value" in html
assert "params.forEach(function(value, key)" in html
assert "configMeta_id_config[key]" in html
assert "consumedConfigKeys.forEach" in html
assert "window.history.replaceState(null, '', cleanUrl.pathname + cleanUrl.search + cleanUrl.hash)" in html
def test_unconfigured_superuser_banner_uses_browser_assisted_setup_wizard():
html = render_to_string(
"core/system_warnings_banner.html",
{
"mode": "unconfigured",
"can_configure": True,
"canonical_host": "archivebox.example.test:8000",
"suggested_base_url": "http://archivebox.example.test:8000",
"machine_admin_url": "/admin/machine/machine/current/change/",
"public_index": True,
"public_add_view": False,
"permissions": "private",
},
)
assert html.count('id="archivebox-setup-wizard"') == 1
assert 'id="archivebox-system-warning-banner"' not in html
assert "⚠ base_url not set" not in html
assert "Setup access to your ArchiveBox Server: <code>archivebox.example.test:8000</code>" in html
assert 'id="archivebox-setup-base-url"' in html
assert 'id="archivebox-setup-security-mode"' in html
assert 'id="archivebox-setup-public-index"' in html
assert 'id="archivebox-setup-public-add"' in html
assert 'id="archivebox-setup-permissions"' in html
assert 'id="archivebox-setup-effective-mode"' in html
assert 'id="archivebox-setup-browser-url"' in html
assert 'id="archivebox-setup-configured-url"' in html
assert 'id="archivebox-setup-url-match"' in html
assert 'name="archivebox-hosting-location"' in html
assert 'name="archivebox-dns-mode"' in html
assert 'name="archivebox-tls-mode"' in html
assert (
html.index('name="archivebox-dns-mode" value="localhost"')
< html.index(
'name="archivebox-dns-mode" value="single"',
)
< html.index('name="archivebox-dns-mode" value="wildcard"')
)
assert (
html.index('name="archivebox-tls-mode" value="localhost"')
< html.index(
'name="archivebox-tls-mode" value="none"',
)
< html.index('name="archivebox-tls-mode" value="single"')
< html.index('name="archivebox-tls-mode" value="wildcard"')
)
assert 'id="archivebox-setup-later"' not in html
assert 'id="archivebox-setup-review" disabled' in html
assert "as BASE_URL below" not in html
assert "Required:" not in html
assert "safe-onedomain-nojsreplay" in html
assert "simplest self-hosted setup" in html
assert "safe-subdomains-fullreplay" in html
assert "unsafe-onedomain-noadmin" in html
assert "danger-onedomain-fullreplay" in html
assert "Wildcard DNS" in html
assert "Modern browsers support wildcard <code>*.archivebox.localhost</code> with no DNS or HTTPS setup required." in html
assert "A archivebox.example.com" in html
assert "JavaScript still runs during capture" in html
assert "will not replay JavaScript unless wildcard DNS is used" in html
assert "Wildcard TLS" in html
assert "How will HTTPS traffic reach this ArchiveBox server?" in html
assert "This mode is not allowed unless also using Single-domain DNS." in html
assert "No separate ingress service / SSL termination" in html
assert "Cloudflare/AWS/Nginx/Caddy/Traefik" in html
assert "In-browser WARC viewing will be disabled unless using <code>localhost</code> or HTTPS" in html
assert "BASE_URL" in html
assert "SERVER_SECURITY_MODE" in html
assert "PUBLIC_INDEX" in html
assert "PUBLIC_ADD_VIEW" in html
assert "Be careful archiving any secret URLs or private share URLs" in html
assert "Beware capturing / viewing archives of URLs that contain malicious JS" in html
assert "PERMISSIONS" in html
assert "/admin/machine/machine/current/change/" in html
assert "setup_wizard.css" in html
assert "setup_wizard.js?v=20260723-2" in html
def test_setup_wizard_assets_enforce_selection_and_access_requirements():
assert "border-color:#15803d; background:#f0fdf4" in SETUP_WIZARD_CSS
assert "accent-color:#15803d" in SETUP_WIZARD_CSS
assert "#archivebox-setup-title code { font-size:inherit; line-height:inherit; }" in SETUP_WIZARD_CSS
assert "updateQuestionDefaults" not in SETUP_WIZARD_JS
assert "canonicalHost" not in SETUP_WIZARD_JS
assert "Browser URL matches BASE_URL." in SETUP_WIZARD_JS
assert "Browser URL matches admin.BASE_URL as expected." in SETUP_WIZARD_JS
assert "Browser URL does not match BASE_URL" in SETUP_WIZARD_JS
assert "saved URLs may contain private share tokens or other secrets." in SETUP_WIZARD_JS
assert "archive intranet URLs" in SETUP_WIZARD_JS
assert "tlsMode === 'single' && dnsMode !== 'single'" in SETUP_WIZARD_JS
assert "Single-domain HTTPS is only allowed with Single-domain DNS." in SETUP_WIZARD_JS
assert "expectedBrowserOrigin: usesSubdomains ? adminOrigin : parsed.origin" in SETUP_WIZARD_JS
assert "Waiting for a matching browser URL and valid setup options" in SETUP_WIZARD_JS
assert "Finish the selected DNS, ingress, and TLS setup" in SETUP_WIZARD_JS
for target in ("adminUrl", "apiUrl", "indexUrl", "snapshotHealthUrl", "originalHealthUrl", "wildcardHealthUrl"):
assert f"probeUrl(preview.{target}" in SETUP_WIZARD_JS
assert "probeUrl(webOrigin + '/web/https://example.com'" not in SETUP_WIZARD_JS
assert "credentials: 'omit'" in SETUP_WIZARD_JS
@pytest.mark.parametrize(
("request_host", "secure", "expected_display_host"),
(
("admin.archivebox.localhost:8000", False, "archivebox.localhost:8000"),
("127.0.0.1:8000", False, "archivebox.localhost:8000"),
("0.0.0.0:8000", False, "archivebox.localhost:8000"),
("archivebox.io:80", False, "archivebox.io"),
("archivebox.io:443", True, "archivebox.io"),
("192.0.2.10:443", True, "192.0.2.10"),
),
)
def test_unconfigured_banner_displays_canonical_url_host(request_host, secure, expected_display_host):
request = RequestFactory().get("/admin/", secure=secure, HTTP_HOST=request_host)
request.user = AnonymousUser()
context = get_setup_wizard_context(request, get_config(include_machine=False))
assert context["display_host"] == expected_display_host
def test_unconfigured_banner_does_not_show_setup_wizard_to_non_superusers():
html = render_to_string(
"core/system_warnings_banner.html",
{
"mode": "unconfigured",
"can_configure": False,
"suggested_base_url": "http://archivebox.example.test:8000",
},
)
assert 'id="archivebox-setup-wizard"' not in html
assert html.count('id="archivebox-system-warning-banner"') == 1
assert "Ask an ArchiveBox superuser to finish server setup" in html
@pytest.mark.parametrize(
("context", "expected_text"),
(
({"mode": "low_disk", "free_gb": "0.50"}, "Only <code"),
({"mode": "high_memory", "mem_pct": "97.0"}, "Virtual memory at"),
({"mode": "high_load", "load_15": "8.0", "load_threshold": 6, "cpu_count": 2}, "15-min loadavg"),
({"mode": "unsafe"}, "ArchiveBox single-domain mode"),
),
)
def test_system_warning_modes_share_one_banner(context, expected_text):
html = render_to_string("core/system_warnings_banner.html", context)
assert html.count('id="archivebox-system-warning-banner"') == 1
assert expected_text in html
def test_configured_base_url_mismatch_banner_shows_both_origins():
config = get_config(include_machine=False).model_copy(update={"BASE_URL": "https://archivebox.example.test"})
request = RequestFactory().get("/admin/", HTTP_HOST="archivebox.internal:8000")
request.user = AnonymousUser()
context = get_base_url_mismatch_context(request, config)
assert context == {
"mode": "base_url_mismatch",
"browser_url": "http://archivebox.internal:8000",
"configured_base_url": "https://archivebox.example.test",
}
assert system_warnings_banner({"CONFIG": config, "request": request}) == context
html = render_to_string("core/system_warnings_banner.html", context)
assert "base_url mismatch" in html
assert "Browser URL:" in html
assert "http://archivebox.internal:8000" in html
assert "Configured BASE_URL:" in html
assert "https://archivebox.example.test" in html
def test_configured_base_url_accepts_its_isolated_admin_subdomain():
config = get_config(include_machine=False).model_copy(
update={"BASE_URL": "https://archivebox.example.test", "SERVER_SECURITY_MODE": "safe-subdomains-fullreplay"},
)
request = RequestFactory().get("/admin/", secure=True, HTTP_HOST="admin.archivebox.example.test")
request.user = AnonymousUser()
assert get_base_url_mismatch_context(request, config) is None
def test_configured_onedomain_base_url_warns_on_admin_alias():
config = get_config(include_machine=False).model_copy(
update={"BASE_URL": "https://archivebox.example.test", "SERVER_SECURITY_MODE": "safe-onedomain-nojsreplay"},
)
request = RequestFactory().get("/admin/", secure=True, HTTP_HOST="admin.archivebox.example.test")
request.user = AnonymousUser()
assert get_base_url_mismatch_context(request, config)["mode"] == "base_url_mismatch"
def test_auto_onedomain_mode_is_not_reported_as_unsafe():
config = get_config(include_machine=False).model_copy(
update={"BASE_URL": "http://archivebox.example.test", "SERVER_SECURITY_MODE": "auto"},
)
assert config.USES_SUBDOMAIN_ROUTING is False
assert system_warnings_banner({"CONFIG": config})["mode"] != "unsafe"