feat(userbot): prereqs at startup, SPA path guard, provision lock
ci / lint-prettier (push) Failing after 8s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Failing after 7s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 5s
deploy / redeploy (push) Failing after 0s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped

- ensure_prerequisites runs on startup, not per-request; kube config
  errors surface as 503 PanelError
- serialize provisioning with a lock; drop per-endpoint prereq checks
- guard SPA fallback against path traversal (relative_to)
- add backend tests for auth flow, k8s service, spa routing; ci comment
  for legacy userbot deployments
This commit is contained in:
2026-09-06 20:38:16 +02:00
parent 861d89d36a
commit d9f1c8325a
10 changed files with 191 additions and 34 deletions
@@ -82,3 +82,20 @@ async def test_string_session_is_validated_and_closed(monkeypatch) -> None:
assert result.session_string == "exported-session"
assert FakeClient.instances[-1].disconnected is True
@pytest.mark.asyncio
async def test_start_phone_rejects_overflow(monkeypatch) -> None:
from app.errors import PanelError
monkeypatch.setattr("app.auth_service.Client", FakeClient)
auth = TelegramAuthService(ttl_seconds=3600, max_flows=2)
await auth.start_phone(phone_payload())
await auth.start_phone(phone_payload())
with pytest.raises(PanelError) as error:
await auth.start_phone(phone_payload())
assert error.value.status_code == 429
assert "Too many pending" in error.value.detail
@@ -80,6 +80,27 @@ def test_partial_provision_rolls_back_only_created_resources() -> None:
kube.apps.delete_namespaced_deployment.assert_not_called()
def test_provision_conflict_reports_existing_instance() -> None:
kube = service()
kube.assert_available = Mock()
kube.apps.create_namespaced_deployment.side_effect = ApiException(status=409)
with pytest.raises(PanelError) as error:
kube.provision(account(), "SESSION")
assert error.value.status_code == 409
assert "already exists" in error.value.detail
# Partial resources created before the 409 must be rolled back.
kube.core.delete_namespaced_secret.assert_called_once_with(
"userbot-test-account-credentials",
"userbot",
)
kube.core.delete_namespaced_persistent_volume_claim.assert_called_once_with(
"userbot-test-account-data",
"userbot",
)
def test_delete_retains_pvc_unless_explicitly_requested() -> None:
kube = service()
deployment = SimpleNamespace(
+48
View File
@@ -0,0 +1,48 @@
from pathlib import Path
import pytest
from app.main import spa_fallback
@pytest.fixture
def static_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
static = tmp_path / "static"
static.mkdir()
(static / "index.html").write_text("<html>index</html>", encoding="utf-8")
(static / "app.js").write_text("console.log('app')", encoding="utf-8")
secret = tmp_path / "secret.txt"
secret.write_text("TOP SECRET", encoding="utf-8")
monkeypatch.setattr("app.main.static_dir", static)
return static
def static_index(static_dir: Path) -> Path:
return static_dir / "index.html"
def test_returns_existing_file(static_dir: Path) -> None:
response = spa_fallback("app.js")
assert response.path == static_dir / "app.js"
def test_unknown_path_falls_back_to_index(static_dir: Path) -> None:
response = spa_fallback("does/not/exist.js")
assert response.path == static_index(static_dir)
def test_traversal_does_not_leak_outside_static(static_dir: Path) -> None:
response = spa_fallback("../secret.txt")
assert response.path == static_index(static_dir)
response = spa_fallback("%2e%2e/secret.txt")
assert response.path == static_index(static_dir)
def test_symlink_outside_static_is_blocked(static_dir: Path, tmp_path: Path) -> None:
target = tmp_path / "outside.txt"
target.write_text("secret", encoding="utf-8")
link = static_dir / "leak.txt"
link.symlink_to(target)
response = spa_fallback("leak.txt")
assert response.path == static_index(static_dir)