from __future__ import annotations import json from pathlib import Path from fastapi.testclient import TestClient import pytest from app.frp import FrpServiceError, FrpValidationError, MockFrpBackend from app.frp.manager import FrpController from app.main import create_app from app.network.diagnostics import MockNetworkDiagnostics VALID_CONFIG = b'serverAddr = "example.invalid"\nserverPort = 7000\n' def test_frp_catalog_defaults_empty_and_persists_multiple_profiles(tmp_path: Path) -> None: backend = MockFrpBackend() controller = FrpController(tmp_path, backend) assert controller.status()["profiles"] == [] assert controller.status()["service"]["enabled"] is False first = controller.catalog.create(filename="support.toml", name="support", content=VALID_CONFIG) second = controller.catalog.create(filename="support.yaml", name="support", content=b"serverAddr: example.invalid\n") assert first["name"] == "support" assert second["name"] == "support (2)" assert controller.status()["selected_id"] == first["id"] reloaded = FrpController(tmp_path, backend) assert [item["name"] for item in reloaded.status()["profiles"]] == ["support", "support (2)"] def test_frp_validation_failure_never_replaces_active_content(tmp_path: Path) -> None: backend = MockFrpBackend() controller = FrpController(tmp_path, backend) profile = controller.catalog.create(filename="support.toml", name="support", content=VALID_CONFIG) controller.start() backend.fail_validation = True with pytest.raises(FrpValidationError): controller.catalog.update(profile["id"], name="changed", content=b"INVALID_FRP_CONFIG") metadata, content = controller.content(profile["id"]) assert metadata["name"] == "support" assert content == VALID_CONFIG assert controller.status()["service"]["running"] is True def test_frp_active_restart_failure_rolls_back_file(tmp_path: Path) -> None: backend = MockFrpBackend() controller = FrpController(tmp_path, backend) profile = controller.catalog.create(filename="support.toml", name="support", content=VALID_CONFIG) controller.start() backend.fail_restart = True with pytest.raises(FrpServiceError): controller.catalog.update(profile["id"], name="changed", content=b'serverAddr = "new.invalid"\n') metadata, content = controller.content(profile["id"]) assert metadata["name"] == "support" assert content == VALID_CONFIG def test_frp_active_profile_cannot_be_deleted(tmp_path: Path) -> None: backend = MockFrpBackend() controller = FrpController(tmp_path, backend) profile = controller.catalog.create(filename="support.toml", name="support", content=VALID_CONFIG) controller.start() with pytest.raises(Exception, match="stop FRP"): controller.catalog.delete(profile["id"]) controller.stop() controller.catalog.delete(profile["id"]) assert controller.status()["profiles"] == [] assert controller.status()["selected_id"] is None def test_frp_damaged_catalog_disables_only_frp_subsystem(tmp_path: Path) -> None: root = tmp_path / "frp" root.mkdir() (root / "catalog.json").write_text("{broken", encoding="utf-8") controller = FrpController(tmp_path, MockFrpBackend()) assert controller.status()["available"] is False assert "preserved" in controller.status()["error"] def test_frp_catalog_rejects_path_escape_and_future_schema_without_touching_files(tmp_path: Path) -> None: root = tmp_path / "frp" profile_id = "eae6f82d-75f8-4a27-8457-e4483f839295" profile_root = root / "profiles" / profile_id profile_root.mkdir(parents=True) outside = root / "escape.toml" outside.write_bytes(VALID_CONFIG) catalog = { "schema_version": 1, "selected_id": profile_id, "profiles": [{ "id": profile_id, "name": "support", "filename": "../../escape.toml", "created_at": "2026-09-07T10:00:00+00:00", "updated_at": "2026-09-07T10:00:00+00:00", }], } (root / "catalog.json").write_text(json.dumps(catalog), encoding="utf-8") controller = FrpController(tmp_path, MockFrpBackend()) assert controller.status()["available"] is False assert outside.read_bytes() == VALID_CONFIG catalog["schema_version"] = 2 (root / "catalog.json").write_text(json.dumps(catalog), encoding="utf-8") future = FrpController(tmp_path, MockFrpBackend()) assert future.status()["available"] is False assert outside.read_bytes() == VALID_CONFIG def test_frp_api_crud_start_stop_and_network_diagnostics(tmp_path: Path) -> None: backend = MockFrpBackend() diagnostic = MockNetworkDiagnostics() app = create_app(project_root=tmp_path, driver_kind="mock", frp_backend=backend, network_diagnostics=diagnostic) with TestClient(app) as client: initial = client.get("/api/frp").json() assert initial["profiles"] == [] assert client.post("/api/frp/service/start").status_code == 409 created = client.post( "/api/frp/configs?filename=support.toml&name=Support", content=VALID_CONFIG, headers={"Content-Type": "application/octet-stream"}, ) assert created.status_code == 201 profile = created.json()["profile"] assert client.get(f"/api/frp/configs/{profile['id']}").content == VALID_CONFIG assert client.post("/api/frp/service/start").json()["frp"]["service"]["enabled"] is True assert client.post("/api/network/diagnostics").json()["overall"] == "ok" assert client.post("/api/frp/service/stop").json()["frp"]["service"]["enabled"] is False assert client.delete(f"/api/frp/configs/{profile['id']}").status_code == 200 @pytest.mark.parametrize("filename", ["x.exe", "../x.toml.exe", "x"]) def test_frp_rejects_unsupported_config_names(tmp_path: Path, filename: str) -> None: app = create_app(project_root=tmp_path, driver_kind="mock") with TestClient(app) as client: response = client.post( f"/api/frp/configs?filename={filename}", content=VALID_CONFIG, headers={"Content-Type": "application/octet-stream"}, ) assert response.status_code == 422