Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 29s
Smoke Test / smoke (pull_request) Failing after 24s
Validate Config / YAML Lint (pull_request) Failing after 21s
Validate Config / JSON Validate (pull_request) Successful in 19s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 1m4s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Cron Syntax Check (pull_request) Successful in 13s
Validate Config / Shell Script Lint (pull_request) Failing after 1m5s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 14s
Validate Config / Playbook Schema Validation (pull_request) Successful in 25s
PR Checklist / pr-checklist (pull_request) Failing after 4m42s
Architecture Lint / Lint Repository (pull_request) Failing after 25s
155 lines
5.4 KiB
Python
155 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unit tests for scripts/gitea_webhook_handler.py.
|
|
Tests core logic: parsing, allowlists, signature verification, idempotency.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from unittest.mock import patch, MagicMock
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent.resolve()
|
|
SPEC = importlib.util.spec_from_file_location(
|
|
"gitea_webhook_handler",
|
|
REPO_ROOT / "scripts" / "gitea_webhook_handler.py",
|
|
)
|
|
WH = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(WH)
|
|
|
|
# Patch CONFIG after module load — the module sets CONFIG = {} at top, then load_config() fills it.
|
|
# For unit tests we inject our own allowlists directly into the module's global CONFIG dict.
|
|
WH.CONFIG.update({
|
|
"webhook_secret": "test-secret-abc123",
|
|
"allowed_repos": {"timmy-config"},
|
|
"allowed_events": {"push", "pull_request", "issues"},
|
|
"allowed_branches": {"refs/heads/main", "refs/heads/master"},
|
|
"allowed_pr_actions": {"opened", "closed", "reopened", "synchronized"},
|
|
"require_signature": True,
|
|
})
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def make_payload(data: dict) -> bytes:
|
|
return json.dumps(data).encode("utf-8")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signature verification
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_verify_signature_valid():
|
|
payload = b'{"test": 1}'
|
|
secret = "s3cret"
|
|
sig = "sha256=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
|
|
assert WH.verify_signature(payload, sig, secret) is True
|
|
|
|
def test_verify_signature_invalid():
|
|
payload = b'{"test": 1}'
|
|
assert WH.verify_signature(payload, "sha256=wrong", "s3cret") is False
|
|
assert WH.verify_signature(payload, "", "s3cret") is False
|
|
assert WH.verify_signature(payload, "md5=abc", "s3cret") is False
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Payload parsing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_parse_payload_valid_push():
|
|
payload = {
|
|
"event": "push",
|
|
"guid": "deliv-123",
|
|
"repository": {"name": "timmy-config"},
|
|
"ref": "refs/heads/main",
|
|
"sender": {"username": "allegro"},
|
|
}
|
|
body = json.dumps(payload).encode()
|
|
event, parsed, repo, delivery = WH.parse_payload(body)
|
|
assert event == "push"
|
|
assert repo == "timmy-config"
|
|
assert delivery == "deliv-123"
|
|
|
|
def test_parse_payload_malformed():
|
|
body = b'not valid json'
|
|
event, parsed, repo, delivery = WH.parse_payload(body)
|
|
assert event is None
|
|
assert parsed == {}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Allowlist checks (use module's patch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_allowed_repo():
|
|
assert WH.allowed_repo("timmy-config") is True
|
|
assert WH.allowed_repo("other-repo") is False
|
|
|
|
def test_allowed_event():
|
|
assert WH.allowed_event("push") is True
|
|
assert WH.allowed_event("unknown") is False
|
|
|
|
def test_branch_allowed():
|
|
assert WH.branch_allowed("refs/heads/main") is True
|
|
assert WH.branch_allowed("refs/heads/dev") is False
|
|
assert WH.branch_allowed(None) is False
|
|
|
|
def test_pr_action_allowed():
|
|
assert WH.pr_action_allowed("opened") is True
|
|
assert WH.pr_action_allowed("edited") is False
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Idempotency DB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_already_processed():
|
|
conn = sqlite3.connect(":memory:")
|
|
conn.execute(
|
|
"""CREATE TABLE webhook_events (
|
|
delivery_id TEXT PRIMARY KEY, received_at TEXT, event_type TEXT,
|
|
repo TEXT, action TEXT, branch TEXT, sender TEXT, verdict TEXT,
|
|
reason TEXT, handler_duration_ms INTEGER
|
|
)"""
|
|
)
|
|
conn.execute("INSERT INTO webhook_events (delivery_id) VALUES ('abc-123')")
|
|
conn.commit()
|
|
assert WH.already_processed(conn, "abc-123") is True
|
|
assert WH.already_processed(conn, "not-exist") is False
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch safety
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_dispatch_push_safe_path():
|
|
"""dispatch_push only calls the hardcoded, safe deploy script."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="OK", stderr="")
|
|
code, msg = WH.dispatch_push("refs/heads/main", "timmy-config")
|
|
assert code == 200
|
|
assert "deploy triggered" in msg
|
|
mock_run.assert_called_once()
|
|
args = mock_run.call_args[0][0]
|
|
# Verify absolute path to safe script
|
|
repo_root = str(REPO_ROOT)
|
|
assert args[-1] == f"{repo_root}/ansible/scripts/deploy_on_webhook.sh"
|
|
|
|
def test_dispatch_push_non_main_rejected():
|
|
code, msg = WH.dispatch_push("refs/heads/dev", "timmy-config")
|
|
assert code == 403
|
|
assert "not in allowed_branches" in msg
|
|
|
|
def test_dispatch_pr_returns_ok():
|
|
code, msg = WH.dispatch_pull_request("opened", 42, "timmy-config")
|
|
assert code == 200
|
|
assert "pr event noted" in msg
|