chore: fix ruff lint (webhooks v2) + Windows-safe test teardown
FlowDeck CI / lint (push) Successful in 1m21s
FlowDeck CI / test (push) Failing after 10m1s
FlowDeck CI / docker (push) Skipped

This commit is contained in:
2026-09-21 21:58:47 -04:00
parent 2fceed0da2
commit b56b181c3e
5 changed files with 40 additions and 44 deletions
+10 -10
View File
@@ -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
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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