Skip to content

Commit 4d2ee59

Browse files
committed
Improve webhook request validation and test coverage
1 parent 8053c92 commit 4d2ee59

File tree

2 files changed

+71
-10
lines changed

2 files changed

+71
-10
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from datetime import datetime
2+
3+
import pytest
4+
5+
import lightspark
6+
7+
8+
class TestWebhooks:
9+
@pytest.mark.parametrize(
10+
"hex_digest",
11+
[
12+
pytest.param(
13+
"62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74",
14+
id="lowercase",
15+
),
16+
pytest.param(
17+
"62A8829AEB48B4142533520B1F7F86CDB1EE7D718BF3EA15BC1C662D4C453B74",
18+
id="uppercase",
19+
),
20+
],
21+
)
22+
def test_valid_data_parses(self, hex_digest: str):
23+
data = b'{"event_type": "NODE_STATUS", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397"}'
24+
secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
25+
26+
webhook = lightspark.WebhookEvent.verify_and_parse(data, hex_digest, secret)
27+
28+
assert (
29+
webhook.entity_id == "lightning_node:01882c25-157a-f96b-0000-362d42b64397"
30+
)
31+
assert webhook.event_id == "1615c8be5aa44e429eba700db2ed8ca5"
32+
assert webhook.event_type == lightspark.WebhookEventType.NODE_STATUS
33+
assert webhook.timestamp == datetime.fromisoformat(
34+
"2023-05-17T23:56:47.874449+00:00"
35+
)
36+
37+
@pytest.mark.parametrize(
38+
"hex_digest",
39+
[
40+
pytest.param("deadbeef", id="wrong length"),
41+
pytest.param("a" * 64, id="incorrect"),
42+
pytest.param("NotAHexString", id="not hex"),
43+
pytest.param(
44+
"62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74"
45+
+ "qq",
46+
id="extra bytes",
47+
),
48+
],
49+
)
50+
def test_invalid_data_raises_value_error(self, hex_digest: str):
51+
data = b'{"event_type": "NODE_STATUS", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397"}'
52+
secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
53+
54+
with pytest.raises(ValueError):
55+
lightspark.WebhookEvent.verify_and_parse(data, hex_digest, secret)
56+
57+
def test_invalid_data_type_raises_type_error(self):
58+
data = 1
59+
hex_digest = "deadbeef"
60+
secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
61+
62+
with pytest.raises(TypeError):
63+
lightspark.WebhookEvent.verify_and_parse(data, hex_digest, secret) # type: ignore

lightspark/webhooks.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,14 @@ class WebhookEvent:
2222

2323
@classmethod
2424
def verify_and_parse(
25-
cls, data: bytes, hexdigest: str, webhook_secret: str
25+
cls, data: bytes, hex_digest: str, webhook_secret: str
2626
) -> "WebhookEvent":
27-
"""Verifies the signature and parses the message into a
28-
WebhookEvent object.
27+
"""Verifies the signature and parses the message into a WebhookEvent object.
2928
3029
Args:
3130
data: the POST message body received by the webhook.
32-
hexdigest: the message signature sent in the
33-
`lightspark-signature` header.
34-
webhook_secret: the webhook secret configured at the
35-
Lightspark API configuration.
31+
hex_digest: the message signature sent in the `lightspark-signature` header.
32+
webhook_secret: the webhook secret configured in the Lightspark API configuration.
3633
3734
Returns:
3835
A parsed WebhookEvent object.
@@ -43,10 +40,11 @@ def verify_and_parse(
4340
if not isinstance(data, bytes):
4441
raise TypeError(f"'data' should be bytes, got {type(data)}")
4542

46-
sig = hmac.new(
43+
mac = hmac.new(
4744
webhook_secret.encode("ascii"), msg=data, digestmod=hashlib.sha256
4845
)
49-
if sig.hexdigest().lower() != hexdigest.lower():
46+
47+
if not hmac.compare_digest(mac.digest(), bytes.fromhex(hex_digest)):
5048
raise ValueError("Webhook message hash does not match signature")
5149

5250
return cls.parse(data)
@@ -73,5 +71,5 @@ def parse(cls, data: bytes) -> "WebhookEvent":
7371
event_id=event["event_id"],
7472
timestamp=datetime.fromisoformat(event["timestamp"]),
7573
entity_id=event["entity_id"],
76-
wallet_id=event["wallet_id"] if "wallet_id" in event else None,
74+
wallet_id=event.get("wallet_id", None),
7775
)

0 commit comments

Comments
 (0)