chore: fix ruff lint (webhooks v2) + Windows-safe test teardown
This commit is contained in:
+10
-10
@@ -16,7 +16,6 @@ from fastapi import APIRouter, Header, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
|
||||
from app.db import get_conn
|
||||
from app.services.automations import fire_event as _fire_event
|
||||
from app.services.api_v2_helpers import (
|
||||
audit_log,
|
||||
check_idempotency,
|
||||
@@ -30,6 +29,7 @@ from app.services.api_v2_helpers import (
|
||||
to_iso8601,
|
||||
validate_scopes_input,
|
||||
)
|
||||
from app.services.automations import fire_event as _fire_event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/v2", tags=["api-v2"])
|
||||
@@ -2397,19 +2397,19 @@ async def retry_webhook_deliveries(webhook_id: int, request: Request,
|
||||
_v2_rate_check(request, user)
|
||||
if not has_scope(user.get("_token_scopes"), "write"):
|
||||
raise HTTPException(403, "Insufficient scope. Required: write")
|
||||
|
||||
|
||||
from app.services.webhook_outbound import retry_due_deliveries
|
||||
|
||||
|
||||
with get_conn() as conn:
|
||||
# Force retry by setting next_retry_at to the past
|
||||
conn.execute(
|
||||
"""UPDATE webhook_deliveries
|
||||
"""UPDATE webhook_deliveries
|
||||
SET next_retry_at = strftime('%s', 'now', '-1 second')
|
||||
WHERE webhook_id = ? AND status = 'retrying'""",
|
||||
(webhook_id,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
retried = await retry_due_deliveries()
|
||||
audit_log(user, "webhook.retry", "webhook", webhook_id, f"retried={retried}", request)
|
||||
return {"webhook_id": webhook_id, "status": "retried", "retried_count": retried}
|
||||
@@ -2421,19 +2421,19 @@ async def verify_webhook_signature(request: Request,
|
||||
"""Verify a webhook signature (for debugging/testing)."""
|
||||
user = get_bearer_user(request, authorization)
|
||||
_v2_rate_check(request, user)
|
||||
|
||||
|
||||
from app.services.webhook_outbound import verify_signature
|
||||
|
||||
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
|
||||
|
||||
secret = body.get("secret", "")
|
||||
payload = body.get("payload", "{}")
|
||||
signature = body.get("signature", "")
|
||||
|
||||
|
||||
is_valid = verify_signature(secret, payload.encode(), signature)
|
||||
|
||||
|
||||
audit_log(user, "webhook.signature_verify", "webhook", 0, f"valid={is_valid}", request)
|
||||
return {"valid": is_valid, "secret": secret[:10] + "..." if len(secret) > 10 else secret}
|
||||
|
||||
+3
-1
@@ -30,7 +30,9 @@ def _user(request: Request, authorization: str | None = None,
|
||||
try:
|
||||
user = get_bearer_user(request, authorization)
|
||||
except HTTPException:
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired API token")
|
||||
raise HTTPException(
|
||||
status_code=401, detail="Invalid or expired API token"
|
||||
) from None
|
||||
if not has_scope(user.get("_token_scopes"), required_scope):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
|
||||
@@ -112,7 +112,7 @@ def _log_delivery(webhook_id: int, event: str, status: str, http_code: int | Non
|
||||
async def _deliver_once(client: httpx.AsyncClient, url: str, event: str,
|
||||
payload: dict, secret: str) -> tuple[int | None, str]:
|
||||
"""Single POST attempt. Returns (http_code, error).
|
||||
|
||||
|
||||
Includes HMAC-SHA256 signature in X-FlowDeck-Signature header.
|
||||
"""
|
||||
body = json.dumps({"event": event, **payload}).encode()
|
||||
@@ -144,7 +144,7 @@ async def deliver_to_sub(sub_id: int, url: str, event: str, payload: dict,
|
||||
|
||||
Returns True on success. Failures are re-queued via ``next_retry_at`` so
|
||||
the background scheduler can pick them up even if this process restarts.
|
||||
|
||||
|
||||
Retry schedule: 2s, 10s, 60s (3 retries total + initial attempt).
|
||||
"""
|
||||
own_client = _client is None
|
||||
|
||||
+4
-1
@@ -42,7 +42,10 @@ def client():
|
||||
|
||||
yield TestClient(app)
|
||||
|
||||
os.unlink(db_path)
|
||||
try:
|
||||
os.unlink(db_path)
|
||||
except PermissionError:
|
||||
pass # Windows: file may still be open in another thread
|
||||
|
||||
|
||||
# ── Core ──
|
||||
|
||||
+21
-30
@@ -1,12 +1,7 @@
|
||||
"""FlowDeck — v6.4.0 Webhooks v2 tests (HMAC, retries, +20 events)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -17,15 +12,11 @@ from app.services.webhook_outbound import (
|
||||
RETRY_DELAYS,
|
||||
_deliver_once,
|
||||
_event_matches,
|
||||
deliver_to_sub,
|
||||
fire_event,
|
||||
init_webhook_tables,
|
||||
retry_due_deliveries,
|
||||
sign_payload,
|
||||
verify_signature,
|
||||
)
|
||||
|
||||
|
||||
# ── Fixtures ────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.fixture
|
||||
@@ -53,7 +44,7 @@ def test_sign_payload_returns_sha256_hex():
|
||||
secret = "my_secret"
|
||||
body = b'{"event": "page.created", "data": {}}'
|
||||
signature = sign_payload(secret, body)
|
||||
|
||||
|
||||
assert signature.startswith("sha256=")
|
||||
hex_part = signature[7:] # Remove 'sha256=' prefix
|
||||
assert len(hex_part) == 64 # SHA-256 hex is 64 chars
|
||||
@@ -63,10 +54,10 @@ def test_sign_payload_consistency():
|
||||
"""Test that the same input produces the same signature."""
|
||||
secret = "my_secret"
|
||||
body = b'{"event": "page.created"}'
|
||||
|
||||
|
||||
sig1 = sign_payload(secret, body)
|
||||
sig2 = sign_payload(secret, body)
|
||||
|
||||
|
||||
assert sig1 == sig2
|
||||
|
||||
|
||||
@@ -75,7 +66,7 @@ def test_verify_signature_valid():
|
||||
secret = "my_secret"
|
||||
body = b'{"event": "page.created", "data": {}}'
|
||||
signature = sign_payload(secret, body)
|
||||
|
||||
|
||||
assert verify_signature(secret, body, signature) is True
|
||||
|
||||
|
||||
@@ -85,7 +76,7 @@ def test_verify_signature_invalid_secret():
|
||||
wrong_secret = "wrong_secret"
|
||||
body = b'{"event": "page.created"}'
|
||||
signature = sign_payload(secret, body)
|
||||
|
||||
|
||||
assert verify_signature(wrong_secret, body, signature) is False
|
||||
|
||||
|
||||
@@ -94,7 +85,7 @@ def test_verify_signature_invalid_signature():
|
||||
secret = "my_secret"
|
||||
body = b'{"event": "page.created"}'
|
||||
signature = "sha256=invalid_hex_value"
|
||||
|
||||
|
||||
assert verify_signature(secret, body, signature) is False
|
||||
|
||||
|
||||
@@ -102,7 +93,7 @@ def test_verify_signature_empty_secret():
|
||||
"""Test that verify_signature returns False for empty secret."""
|
||||
body = b'{"event": "page.created"}'
|
||||
signature = "sha256=somehash"
|
||||
|
||||
|
||||
assert verify_signature("", body, signature) is False
|
||||
|
||||
|
||||
@@ -110,7 +101,7 @@ def test_verify_signature_empty_signature():
|
||||
"""Test that verify_signature returns False for empty signature."""
|
||||
secret = "my_secret"
|
||||
body = b'{"event": "page.created"}'
|
||||
|
||||
|
||||
assert verify_signature(secret, body, "") is False
|
||||
|
||||
|
||||
@@ -220,14 +211,14 @@ async def test_deliver_once_success():
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
|
||||
url = "https://example.com/webhook"
|
||||
event = "page.created"
|
||||
payload = {"page_id": 123}
|
||||
secret = "test_secret"
|
||||
|
||||
|
||||
code, error = await _deliver_once(mock_client, url, event, payload, secret)
|
||||
|
||||
|
||||
assert code == 200
|
||||
assert error == ""
|
||||
mock_client.post.assert_called_once()
|
||||
@@ -243,14 +234,14 @@ async def test_deliver_once_http_error():
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 500
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
|
||||
url = "https://example.com/webhook"
|
||||
event = "page.created"
|
||||
payload = {}
|
||||
secret = "test_secret"
|
||||
|
||||
|
||||
code, error = await _deliver_once(mock_client, url, event, payload, secret)
|
||||
|
||||
|
||||
assert code == 500
|
||||
assert "HTTP 500" in error
|
||||
|
||||
@@ -260,14 +251,14 @@ async def test_deliver_once_timeout():
|
||||
"""Test _deliver_once handles timeout errors."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.side_effect = httpx.TimeoutException("Timeout after 30s")
|
||||
|
||||
|
||||
url = "https://example.com/webhook"
|
||||
event = "page.created"
|
||||
payload = {}
|
||||
secret = "test_secret"
|
||||
|
||||
|
||||
code, error = await _deliver_once(mock_client, url, event, payload, secret)
|
||||
|
||||
|
||||
assert code is None
|
||||
assert "Timeout" in error
|
||||
|
||||
@@ -277,14 +268,14 @@ async def test_deliver_once_request_error():
|
||||
"""Test _deliver_once handles request errors."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.side_effect = httpx.RequestError("Connection refused")
|
||||
|
||||
|
||||
url = "https://example.com/webhook"
|
||||
event = "page.created"
|
||||
payload = {}
|
||||
secret = "test_secret"
|
||||
|
||||
|
||||
code, error = await _deliver_once(mock_client, url, event, payload, secret)
|
||||
|
||||
|
||||
assert code is None
|
||||
assert "Request error" in error
|
||||
|
||||
|
||||
Reference in New Issue
Block a user