feat(SEC-001): protect workspace bootstrap with turnstile
This commit is contained in:
153
tests/test_turnstile.py
Normal file
153
tests/test_turnstile.py
Normal file
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import requests
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
def test_legacy_get_bootstrap_url_redirects_to_welcome_page() -> None:
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/workspaces/bootstrap", follow_redirects=False)
|
||||
|
||||
assert response.status_code in {302, 303, 307}
|
||||
assert response.headers["location"] == "/"
|
||||
|
||||
|
||||
def test_bootstrap_workspace_rejects_missing_turnstile_token(monkeypatch, tmp_path) -> None:
|
||||
from app.models import workspace as workspace_module
|
||||
from app.models.workspace import WorkspaceRepository
|
||||
|
||||
repo = WorkspaceRepository(base_path=tmp_path / "workspaces")
|
||||
monkeypatch.setattr(workspace_module, "_workspace_repo", repo)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/workspaces/bootstrap", follow_redirects=False)
|
||||
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/?captcha_error=1"
|
||||
|
||||
|
||||
def test_bootstrap_workspace_creates_workspace_when_turnstile_verification_succeeds(monkeypatch, tmp_path) -> None:
|
||||
from app.models import workspace as workspace_module
|
||||
from app.models.workspace import WorkspaceRepository
|
||||
from app.services import turnstile as turnstile_module
|
||||
|
||||
repo = WorkspaceRepository(base_path=tmp_path / "workspaces")
|
||||
monkeypatch.setattr(workspace_module, "_workspace_repo", repo)
|
||||
monkeypatch.setattr(turnstile_module, "verify_turnstile_token", lambda *args, **kwargs: True)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/workspaces/bootstrap",
|
||||
data={"cf-turnstile-response": "token-ok"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code in {302, 303, 307}
|
||||
workspace_id = response.headers["location"].strip("/")
|
||||
assert repo.workspace_exists(workspace_id)
|
||||
assert response.cookies.get("workspace_id") == workspace_id
|
||||
|
||||
|
||||
def test_bootstrap_workspace_rejects_failed_turnstile_verification(monkeypatch, tmp_path) -> None:
|
||||
from app.models import workspace as workspace_module
|
||||
from app.models.workspace import WorkspaceRepository
|
||||
from app.services import turnstile as turnstile_module
|
||||
|
||||
repo = WorkspaceRepository(base_path=tmp_path / "workspaces")
|
||||
monkeypatch.setattr(workspace_module, "_workspace_repo", repo)
|
||||
monkeypatch.setattr(turnstile_module, "verify_turnstile_token", lambda *args, **kwargs: False)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/workspaces/bootstrap",
|
||||
data={"cf-turnstile-response": "token-bad"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/?captcha_error=1"
|
||||
assert not any(repo.base_path.iterdir())
|
||||
|
||||
|
||||
def test_turnstile_settings_use_default_test_keys_when_env_is_missing(monkeypatch) -> None:
|
||||
from app.services import turnstile as turnstile_module
|
||||
|
||||
monkeypatch.setenv("APP_ENV", "test")
|
||||
monkeypatch.setenv("APP_ENV", "test")
|
||||
monkeypatch.delenv("TURNSTILE_SITE_KEY", raising=False)
|
||||
monkeypatch.delenv("TURNSTILE_SECRET_KEY", raising=False)
|
||||
|
||||
settings = turnstile_module.load_turnstile_settings()
|
||||
|
||||
assert settings.site_key == turnstile_module.DEFAULT_TURNSTILE_TEST_SITE_KEY
|
||||
assert settings.secret_key == turnstile_module.DEFAULT_TURNSTILE_TEST_SECRET_KEY
|
||||
assert settings.enabled is True
|
||||
assert settings.uses_test_keys is True
|
||||
assert settings.uses_test_keys is True
|
||||
|
||||
|
||||
def test_turnstile_settings_fail_loudly_without_keys_outside_dev(monkeypatch) -> None:
|
||||
from app.services import turnstile as turnstile_module
|
||||
|
||||
monkeypatch.setenv("APP_ENV", "production")
|
||||
monkeypatch.delenv("TURNSTILE_SITE_KEY", raising=False)
|
||||
monkeypatch.delenv("TURNSTILE_SECRET_KEY", raising=False)
|
||||
|
||||
import pytest
|
||||
|
||||
with pytest.raises(RuntimeError, match="Turnstile keys must be configured"):
|
||||
turnstile_module.load_turnstile_settings()
|
||||
|
||||
|
||||
def test_turnstile_verification_returns_false_on_transport_error(monkeypatch) -> None:
|
||||
from app.services import turnstile as turnstile_module
|
||||
|
||||
monkeypatch.setenv("APP_ENV", "test")
|
||||
|
||||
def raise_error(*args, **kwargs):
|
||||
raise requests.RequestException("boom")
|
||||
|
||||
monkeypatch.setattr(turnstile_module.requests, "post", raise_error)
|
||||
|
||||
assert turnstile_module.verify_turnstile_token("token") is False
|
||||
|
||||
|
||||
def test_turnstile_settings_support_always_fail_test_keys(monkeypatch) -> None:
|
||||
from app.services import turnstile as turnstile_module
|
||||
|
||||
monkeypatch.setenv("APP_ENV", "test")
|
||||
monkeypatch.setenv("TURNSTILE_SITE_KEY", turnstile_module.ALWAYS_FAIL_TURNSTILE_TEST_SITE_KEY)
|
||||
monkeypatch.setenv("TURNSTILE_SECRET_KEY", turnstile_module.ALWAYS_FAIL_TURNSTILE_TEST_SECRET_KEY)
|
||||
|
||||
settings = turnstile_module.load_turnstile_settings()
|
||||
|
||||
assert settings.site_key == turnstile_module.ALWAYS_FAIL_TURNSTILE_TEST_SITE_KEY
|
||||
assert settings.secret_key == turnstile_module.ALWAYS_FAIL_TURNSTILE_TEST_SECRET_KEY
|
||||
assert settings.enabled is True
|
||||
assert settings.uses_test_keys is False
|
||||
|
||||
|
||||
def test_bootstrap_stays_blocked_under_always_fail_turnstile_test_keys(monkeypatch, tmp_path) -> None:
|
||||
from app.models import workspace as workspace_module
|
||||
from app.models.workspace import WorkspaceRepository
|
||||
from app.services import turnstile as turnstile_module
|
||||
|
||||
repo = WorkspaceRepository(base_path=tmp_path / "workspaces")
|
||||
monkeypatch.setattr(workspace_module, "_workspace_repo", repo)
|
||||
monkeypatch.setenv("APP_ENV", "test")
|
||||
monkeypatch.setenv("TURNSTILE_SITE_KEY", turnstile_module.ALWAYS_FAIL_TURNSTILE_TEST_SITE_KEY)
|
||||
monkeypatch.setenv("TURNSTILE_SECRET_KEY", turnstile_module.ALWAYS_FAIL_TURNSTILE_TEST_SECRET_KEY)
|
||||
monkeypatch.setattr(turnstile_module, "verify_turnstile_token", lambda *args, **kwargs: False)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/workspaces/bootstrap",
|
||||
data={"cf-turnstile-response": "blocked-token"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/?captcha_error=1"
|
||||
assert not any(repo.base_path.iterdir())
|
||||
Reference in New Issue
Block a user