309 lines
7.4 KiB
Python
309 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app.main as main
|
|
from scripts.rebuild_index import ReindexInProgressError
|
|
|
|
|
|
WEBHOOK_SECRET = "w" * 64
|
|
SYNC_API_KEY = "s" * 64
|
|
|
|
|
|
def fake_rebuild_result() -> dict:
|
|
return {
|
|
"duration_seconds": 0.01,
|
|
"counts": {
|
|
"documents": 1,
|
|
"chunks": 1,
|
|
"fts_chunks": 1,
|
|
"tags": 0,
|
|
"categories": 0,
|
|
},
|
|
}
|
|
|
|
|
|
def sign(body: bytes) -> str:
|
|
return hmac.new(
|
|
WEBHOOK_SECRET.encode("utf-8"),
|
|
body,
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
|
|
|
|
@pytest.fixture
|
|
def client() -> TestClient:
|
|
with TestClient(main.app) as test_client:
|
|
yield test_client
|
|
|
|
|
|
def test_startup_rejects_missing_secret(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.delenv("WEBHOOK_SECRET")
|
|
|
|
with pytest.raises(RuntimeError, match="WEBHOOK_SECRET"):
|
|
with TestClient(main.app):
|
|
pass
|
|
|
|
|
|
def test_startup_rejects_short_secret(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setenv("SYNC_API_KEY", "short")
|
|
|
|
with pytest.raises(RuntimeError, match="aspoň 32"):
|
|
with TestClient(main.app):
|
|
pass
|
|
|
|
|
|
def test_health_endpoint(client: TestClient) -> None:
|
|
response = client.get("/health")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["status"] == "ok"
|
|
assert payload["search_engine"] == "sqlite_fts5"
|
|
assert payload["security_configured"] is True
|
|
|
|
|
|
def test_search_endpoint_uses_shared_search_logic(
|
|
client: TestClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
main,
|
|
"search_database",
|
|
lambda *args, **kwargs: {
|
|
"engine": "sqlite_fts5",
|
|
"strategies": ["all_terms"],
|
|
"results": [{"chunk_id": "test::0", "published": True}],
|
|
},
|
|
)
|
|
|
|
response = client.post(
|
|
"/search",
|
|
json={"query": "jan ptak", "limit": 5},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["count"] == 1
|
|
assert response.json()["results"][0]["chunk_id"] == "test::0"
|
|
|
|
|
|
def test_search_rejects_empty_query(client: TestClient) -> None:
|
|
response = client.post("/search", json={"query": ""})
|
|
assert response.status_code == 422
|
|
|
|
|
|
def test_sync_rejects_missing_api_key(client: TestClient) -> None:
|
|
response = client.post("/sync", json={"pull_git": False})
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_sync_rejects_wrong_api_key(client: TestClient) -> None:
|
|
response = client.post(
|
|
"/sync",
|
|
headers={"X-API-Key": "x" * 64},
|
|
json={"pull_git": False},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_sync_accepts_valid_api_key(
|
|
client: TestClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
main,
|
|
"rebuild_index",
|
|
lambda pull_git=False: fake_rebuild_result(),
|
|
)
|
|
|
|
response = client.post(
|
|
"/sync",
|
|
headers={"X-API-Key": SYNC_API_KEY},
|
|
json={"pull_git": False},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "ok"
|
|
|
|
|
|
def test_sync_returns_conflict_when_reindex_is_running(
|
|
client: TestClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
def busy(*args, **kwargs):
|
|
raise ReindexInProgressError("Reindexovanie už prebieha")
|
|
|
|
monkeypatch.setattr(main, "rebuild_index", busy)
|
|
|
|
response = client.post(
|
|
"/sync",
|
|
headers={"X-API-Key": SYNC_API_KEY},
|
|
json={"pull_git": False},
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
def test_webhook_rejects_invalid_signature(client: TestClient) -> None:
|
|
body = json.dumps(
|
|
{"repository": {"full_name": "KEMT/zpwiki"}}
|
|
).encode("utf-8")
|
|
|
|
response = client.post(
|
|
"/webhook/gitea",
|
|
content=body,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Gitea-Event": "push",
|
|
"X-Gitea-Signature": "0" * 64,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_webhook_rejects_invalid_json_with_valid_signature(
|
|
client: TestClient,
|
|
) -> None:
|
|
body = b"not-json"
|
|
|
|
response = client.post(
|
|
"/webhook/gitea",
|
|
content=body,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Gitea-Event": "push",
|
|
"X-Gitea-Signature": sign(body),
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_webhook_requires_event_header(client: TestClient) -> None:
|
|
body = json.dumps(
|
|
{"repository": {"full_name": "KEMT/zpwiki"}}
|
|
).encode("utf-8")
|
|
|
|
response = client.post(
|
|
"/webhook/gitea",
|
|
content=body,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Gitea-Signature": sign(body),
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_webhook_ignores_non_push_event(
|
|
client: TestClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
calls = 0
|
|
|
|
def fake_rebuild(*args, **kwargs):
|
|
nonlocal calls
|
|
calls += 1
|
|
return fake_rebuild_result()
|
|
|
|
monkeypatch.setattr(main, "rebuild_index", fake_rebuild)
|
|
body = json.dumps(
|
|
{"repository": {"full_name": "KEMT/zpwiki"}}
|
|
).encode("utf-8")
|
|
|
|
response = client.post(
|
|
"/webhook/gitea",
|
|
content=body,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Gitea-Event": "issues",
|
|
"X-Gitea-Signature": sign(body),
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 202
|
|
assert response.json()["status"] == "ignored"
|
|
assert calls == 0
|
|
|
|
|
|
def test_webhook_rejects_unexpected_repository(client: TestClient) -> None:
|
|
body = json.dumps(
|
|
{"repository": {"full_name": "OTHER/repository"}}
|
|
).encode("utf-8")
|
|
|
|
response = client.post(
|
|
"/webhook/gitea",
|
|
content=body,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Gitea-Event": "push",
|
|
"X-Gitea-Signature": sign(body),
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_webhook_accepts_signed_push(
|
|
client: TestClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
main,
|
|
"rebuild_index",
|
|
lambda pull_git=False: fake_rebuild_result(),
|
|
)
|
|
body = json.dumps(
|
|
{"repository": {"full_name": "KEMT/zpwiki"}}
|
|
).encode("utf-8")
|
|
|
|
response = client.post(
|
|
"/webhook/gitea",
|
|
content=body,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Gitea-Event": "push",
|
|
"X-Gitea-Signature": sign(body),
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["verified_by"] == "hmac_sha256"
|
|
assert response.json()["repository"] == "KEMT/zpwiki"
|
|
|
|
|
|
def test_webhook_returns_conflict_when_reindex_is_running(
|
|
client: TestClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
def busy(*args, **kwargs):
|
|
raise ReindexInProgressError("Reindexovanie už prebieha")
|
|
|
|
monkeypatch.setattr(main, "rebuild_index", busy)
|
|
body = json.dumps(
|
|
{"repository": {"full_name": "KEMT/zpwiki"}}
|
|
).encode("utf-8")
|
|
|
|
response = client.post(
|
|
"/webhook/gitea",
|
|
content=body,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Gitea-Event": "push",
|
|
"X-Gitea-Signature": sign(body),
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 409
|