146 lines
6.1 KiB
Python
146 lines
6.1 KiB
Python
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
|