"""Tests for RouterOS RSC export parser.""" from app.services.rsc_parser import parse_rsc, validate_rsc, compute_impact SAMPLE_EXPORT = """\ # 2026-03-07 12:00:00 by RouterOS 7.16.2 # software id = ABCD-1234 # # model = RB750Gr3 /interface bridge add name=bridge1 /ip address add address=192.168.88.1/24 interface=ether1 network=192.168.88.0 add address=10.0.0.1/24 interface=bridge1 network=10.0.0.0 /ip firewall filter add action=accept chain=input comment="allow established" \\ connection-state=established,related add action=drop chain=input in-interface-list=WAN /ip dns set servers=8.8.8.8,8.8.4.4 /system identity set name=test-router """ class TestParseRsc: def test_extracts_categories(self): result = parse_rsc(SAMPLE_EXPORT) paths = [c["path"] for c in result["categories"]] assert "/interface bridge" in paths assert "/ip address" in paths assert "/ip firewall filter" in paths assert "/ip dns" in paths assert "/system identity" in paths def test_counts_commands_per_category(self): result = parse_rsc(SAMPLE_EXPORT) cat_map = {c["path"]: c for c in result["categories"]} assert cat_map["/ip address"]["adds"] == 2 assert cat_map["/ip address"]["sets"] == 0 assert cat_map["/ip firewall filter"]["adds"] == 2 assert cat_map["/ip dns"]["sets"] == 1 assert cat_map["/system identity"]["sets"] == 1 def test_handles_continuation_lines(self): result = parse_rsc(SAMPLE_EXPORT) cat_map = {c["path"]: c for c in result["categories"]} # The firewall filter has a continuation line — should still count as 2 adds assert cat_map["/ip firewall filter"]["adds"] == 2 def test_ignores_comments_and_blank_lines(self): result = parse_rsc(SAMPLE_EXPORT) # Comments at top should not create categories paths = [c["path"] for c in result["categories"]] assert "#" not in paths def test_empty_input(self): result = parse_rsc("") assert result["categories"] == [] class TestValidateRsc: def test_valid_export_passes(self): result = validate_rsc(SAMPLE_EXPORT) assert result["valid"] is True assert result["errors"] == [] def test_unbalanced_quotes_detected(self): bad = '/system identity\nset name="missing-end-quote\n' result = validate_rsc(bad) assert result["valid"] is False assert any("quote" in e.lower() for e in result["errors"]) def test_truncated_continuation_detected(self): bad = "/ip address\nadd address=192.168.1.1/24 \\\n" result = validate_rsc(bad) assert result["valid"] is False assert any("truncat" in e.lower() or "continuation" in e.lower() for e in result["errors"]) class TestComputeImpact: def test_high_risk_for_firewall_input(self): current = "/ip firewall filter\nadd action=accept chain=input\n" target = "/ip firewall filter\nadd action=drop chain=input\n" result = compute_impact(parse_rsc(current), parse_rsc(target)) assert any(c["risk"] == "high" for c in result["categories"]) def test_high_risk_for_ip_address_changes(self): current = "/ip address\nadd address=192.168.1.1/24 interface=ether1\n" target = "/ip address\nadd address=10.0.0.1/24 interface=ether1\n" result = compute_impact(parse_rsc(current), parse_rsc(target)) ip_cat = next(c for c in result["categories"] if c["path"] == "/ip address") assert ip_cat["risk"] in ("high", "medium") def test_warnings_for_management_access(self): current = "" target = "/ip firewall filter\nadd action=drop chain=input protocol=tcp dst-port=22\n" result = compute_impact(parse_rsc(current), parse_rsc(target)) assert len(result["warnings"]) > 0 def test_no_changes_no_warnings(self): same = "/ip dns\nset servers=8.8.8.8\n" result = compute_impact(parse_rsc(same), parse_rsc(same)) assert result["warnings"] == [] or all(c["risk"] == "none" for c in result["categories"])