Files
hermes-webui/tests/test_issue4571_mic_insecure_origin.py
T

115 lines
4.2 KiB
Python

"""Regression tests for #4571 insecure-origin microphone messaging.
Browser speech/microphone APIs can report permission-style errors when the real
problem is that the page was opened over insecure HTTP from a non-local origin.
The UI must explain the HTTPS/localhost requirement instead of only saying the
browser permission was denied.
"""
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[1]
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
def _locale_count() -> int:
return len(re.findall(r"^ ['\"]?[\w-]+['\"]?: \{", I18N_JS, re.MULTILINE))
def _slice_between(src: str, start_marker: str, end_marker: str) -> str:
start = src.index(start_marker)
end = src.index(end_marker, start)
return src[start:end]
def test_i18n_key_exists_in_every_locale_and_names_https_localhost():
assert I18N_JS.count("mic_insecure_origin") == _locale_count()
english = re.search(r"mic_insecure_origin: '([^']+)'", I18N_JS)
assert english, "English mic_insecure_origin string must exist"
value = english.group(1).lower()
assert "https" in value
assert "localhost" in value
assert "microphone" in value
def test_secure_context_and_localhost_loopback_are_explicitly_preserved():
assert "function _micOriginNeedsSecureContext()" in BOOT_JS
assert "window.isSecureContext===true" in BOOT_JS
assert "protocol==='http:'" in BOOT_JS
assert "!_micIsLocalhostOrLoopback(loc.hostname)" in BOOT_JS
localhost_helper = _slice_between(
BOOT_JS,
"function _micIsLocalhostOrLoopback(hostname)",
"\n}\n\nfunction _micOriginNeedsSecureContext",
)
assert "host==='localhost'" in localhost_helper
assert "host.endsWith('.localhost')" in localhost_helper
assert "host==='::1'" in localhost_helper
assert "/^127\\./.test(host)" in localhost_helper
def test_permission_style_speech_errors_use_insecure_origin_key_only_on_that_branch():
helper = _slice_between(
BOOT_JS,
"function _micToastKeyForRecognitionError(error)",
"\n}\n\n(function(){",
)
for error in ("not-allowed", "service-not-allowed", "audio-capture"):
assert error in helper
assert "_micOriginNeedsSecureContext()" in helper
assert "return 'mic_insecure_origin';" in helper
assert "'audio-capture':'mic_denied'" not in helper
assert "'network':'mic_network'" in helper
assert "'no-speech':'mic_no_speech'" in helper
def test_dictation_preflights_insecure_origin_before_speech_or_media_capture():
body = _slice_between(
BOOT_JS,
"async function _startMicCapture(holdRequired=false){",
"\n\n async function _toggleMicCapture(){",
)
insecure_idx = body.index("_micOriginNeedsSecureContext()")
speech_idx = body.index("recognition.start()")
media_idx = body.index("navigator.mediaDevices.getUserMedia")
assert insecure_idx < speech_idx
assert insecure_idx < media_idx
assert "showToast(t('mic_insecure_origin'))" in body
def test_dictation_speechrecognition_errors_route_through_shared_helper():
body = _slice_between(
BOOT_JS,
"sr.onerror=(event)=>{",
"\n return sr;",
)
assert "_micToastKeyForRecognitionError(event.error)" in body
assert "messageKey?t(messageKey):t('mic_error')+event.error" in body
def test_voice_mode_preflights_and_routes_permission_errors_through_shared_helper():
activate_body = _slice_between(
BOOT_JS,
"function _activate(){",
"\n function _deactivate(){",
)
assert "_micOriginNeedsSecureContext()" in activate_body
assert "showToast(t('mic_insecure_origin'))" in activate_body
assert "_voiceModeActive=true" in activate_body
assert activate_body.index("_micOriginNeedsSecureContext()") < activate_body.index("_voiceModeActive=true")
start_body = _slice_between(
BOOT_JS,
"function _startListening(){",
"\n function _voiceModeSend(){",
)
assert "_micOriginNeedsSecureContext()" in start_body
assert "showToast(t('mic_insecure_origin'))" in start_body
assert "_micToastKeyForRecognitionError(event.error)" in start_body
assert "messageKey?t(messageKey):t('mic_error')+event.error" in start_body