"""`shared/cameraMakes.js` is imported by BOTH camera forms and has no test runner of its own. This is the same safety net `test_dashboard.py` provides: node is driven from pytest and the check is skipped when node is absent, so the suite stays dependency-light. What it guards is the field people actually have. The engine has always accepted a whole RTSP URL (`CameraConfig.url` wins over the parts) and no form ever offered one, so an operator holding the address their camera's own app shows had to take it apart into five fields by eye — which is exactly where a password containing `@` goes wrong, a class of bug this repository has already been bitten by once. """ import json import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent SHARED = ROOT / "shared" / "cameraMakes.js" pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node not installed") def parse(text): out = subprocess.run( ["node", "--input-type=module", "-e", f"import {{ parseRtspUrl }} from {json.dumps(str(SHARED))};" f"console.log(JSON.stringify(parseRtspUrl({json.dumps(text)})))"], capture_output=True, text=True, timeout=60) assert out.returncode == 0, out.stderr return json.loads(out.stdout.strip()) def test_a_plain_url_becomes_the_five_fields(): assert parse("rtsp://admin:Pass123@192.168.1.121:554/ch0_0.264") == { "host": "192.168.1.121", "port": 554, "path": "/ch0_0.264", "username": "admin", "password": "Pass123"} def test_an_at_sign_in_the_password_survives(): """The one that matters. A URL is split at the LAST `@`, which is what makes an unencoded `@` inside a password parse the way a person means it - and an operator splitting this by eye would put `p` in the password box and `ssw0rd@192.168.1.121` in the address box.""" got = parse("rtsp://admin:p@ssw0rd@192.168.1.121:554/Streaming/Channels/101") assert got["password"] == "p@ssw0rd" assert got["host"] == "192.168.1.121" def test_a_percent_encoded_password_is_decoded(): """Stored decoded, because CameraConfig.source() percent-encodes when it rebuilds the URL. Keeping it encoded would double-encode it and the camera would refuse a password that is correct.""" assert parse("rtsp://admin:p%40ss@10.0.0.5/live")["password"] == "p@ss" def test_the_default_port_is_filled_in(): assert parse("rtsp://192.168.1.122/ch0_1.264")["port"] == 554 def test_a_query_string_stays_with_the_path(): """Some cameras carry the channel in a query. Dropping it opens the wrong channel, which looks like a camera pointed somewhere unexpected.""" assert parse("rtsp://cam.local:8554/live?channel=1")["path"] == "/live?channel=1" def test_a_scheme_is_optional_but_structure_is_not(): assert parse("192.168.1.122/ch0_1.264")["host"] == "192.168.1.122" # A bare word parses as a perfectly good hostname, so without this it # would silently fill the Address field with it - a wrong answer that # looks like it worked, which is worse than refusing. assert parse("nonsense") is None assert parse("192.168.1.121") is None, "a bare address is not a URL; the Address field takes it" def test_nothing_is_nothing(): assert parse("") is None assert parse(" ") is None def test_both_forms_import_it(): """Two copies of this would be worse than not offering it, because an operator trusts a filled-in field. Same rule as the make picker.""" for form in (ROOT / "desktop/frontend/src/views/Cameras.jsx", ROOT / "web/src/views/CameraSetup.jsx"): src = form.read_text(encoding="utf-8") assert "parseRtspUrl" in src, f"{form.name} does not offer the paste field" assert "cameraMakes.js" in src, f"{form.name} defines its own parser"