#!/usr/bin/env python3 """Webhook smoke probe for `harness events`. Drives the receiver on BASE (default http://127.0.0.1:4100) with plain stdlib urllib: the host has no curl by policy. Checks: 401 without secrets, 200 + stored with correct secrets, duplicate replay, per-provider action mapping, healthz. Prints one PASS/FAIL line per check; exits non-zero on any failure. """ import hashlib import hmac import json import os import sys import time import urllib.error import urllib.request BASE = os.environ.get("BASE_URL", "http://127.0.0.1:4100") GITEA_SECRET = os.environ.get("GITEA_SECRET", "smoke-gitea-secret") REDMINE_SECRET = os.environ.get("REDMINE_SECRET", "smoke-redmine-secret") DISCOURSE_SECRET = os.environ.get("DISCOURSE_SECRET", "smoke-discourse-secret") failures = 0 def req(method, path, body=b"", headers=None): r = urllib.request.Request(BASE + path, data=body if method == "POST" else None, method=method, headers=headers or {}) try: with urllib.request.urlopen(r, timeout=5) as resp: return resp.status, resp.read().decode() except urllib.error.HTTPError as e: return e.code, e.read().decode() def check(name, cond, detail=""): global failures print(("PASS" if cond else "FAIL"), name, ("- " + detail) if detail else "") if not cond: failures += 1 def wait_healthz(tries=50): for _ in range(tries): try: code, _ = req("GET", "/healthz") if code == 200: return True except Exception: pass time.sleep(0.2) return False def main(): check("healthz answers", wait_healthz()) gitea_body = json.dumps({ "action": "approved", "number": 5, "pull_request": {"number": 5, "title": "smoke PR", "merged": False}, "repository": {"full_name": "ukrrs/MOPAC"}, "sender": {"login": "smoke-charles"}, }).encode() code, text = req("POST", "/hooks/gitea", gitea_body) check("gitea unsigned rejected 401", code == 401, text.strip()) bad_sig = "0" * 64 code, text = req("POST", "/hooks/gitea", gitea_body, {"X-Gitea-Signature": bad_sig}) check("gitea bad signature rejected 401", code == 401, text.strip()) sig = hmac.new(GITEA_SECRET.encode(), gitea_body, hashlib.sha256).hexdigest() hdrs = {"X-Gitea-Signature": sig, "X-Gitea-Delivery": "smoke-delivery-1"} code, text = req("POST", "/hooks/gitea", gitea_body, hdrs) flat = text.replace(" ", "") check("gitea signed 200 stored", code == 200 and '"status":"stored"' in flat, text.strip()) check("gitea maps to pipeline_step", '"action":"pipeline_step"' in flat, text.strip()) code, text = req("POST", "/hooks/gitea", gitea_body, hdrs) flat = text.replace(" ", "") check("gitea replay deduped", code == 200 and '"status":"duplicate"' in flat, text.strip()) redmine_body = json.dumps({ "event_name": "issue_updated", "payload": {"issue": {"id": 42, "subject": "smoke issue"}, "user": {"login": "smoke-charles"}}, }).encode() code, text = req("POST", "/hooks/redmine", redmine_body, {"X-Redmine-Webhook-Secret": REDMINE_SECRET}) flat = text.replace(" ", "") check("redmine shared secret 200 stored", code == 200 and '"status":"stored"' in flat, text.strip()) check("redmine maps to dispatch_turn", '"action":"dispatch_turn"' in flat, text.strip()) code, text = req("POST", "/hooks/redmine", redmine_body, {"X-Redmine-Webhook-Secret": "wrong-secret"}) check("redmine wrong secret rejected 401", code == 401, text.strip()) discourse_body = json.dumps({ "post": {"id": 9, "topic_id": 7, "username": "smoke-charles", "topic_title": "smoke topic"}, }).encode() code, text = req("POST", "/hooks/discourse", discourse_body, { "X-Discourse-Webhook-Secret": DISCOURSE_SECRET, "X-Discourse-Event": "post_created", "X-Discourse-Event-Id": "smoke-post-9", }) flat = text.replace(" ", "") check("discourse shared secret 200 stored", code == 200 and '"status":"stored"' in flat, text.strip()) check("discourse maps to respond_turn", '"action":"respond_turn"' in flat, text.strip()) code, _ = req("GET", "/hooks/gitea") check("GET hook rejected 405", code == 405) print("probe:", "OK" if failures == 0 else f"{failures} FAILURE(S)") return 0 if failures == 0 else 1 if __name__ == "__main__": sys.exit(main())