44
55import os
66from typing import Any , cast
7- from datetime import datetime , timedelta
7+ from datetime import datetime , timezone , timedelta
88
99import pytest
1010import time_machine
@@ -18,7 +18,11 @@ class TestWebhooks:
1818 parametrize = pytest .mark .parametrize ("client" , [False , True ], indirect = True , ids = ["loose" , "strict" ])
1919
2020 timestamp = "2024-03-27T15:42:29.551"
21- fake_now = datetime .fromisoformat (timestamp ).astimezone ()
21+ # Fix: Ensure fake_now matches how webhook timestamps are now parsed (UTC assumption)
22+ fake_now_dt = datetime .fromisoformat (timestamp )
23+ if fake_now_dt .tzinfo is None :
24+ fake_now_dt = fake_now_dt .replace (tzinfo = timezone .utc )
25+ fake_now = fake_now_dt .astimezone ()
2226
2327 payload = """{"id": "o4mmewpfNNTnjfZc", "created_at": "2024-03-27T15:42:29+00:00", "type": "resource_event.test", "properties": {"message": "A test webhook from Orb. Happy testing!"}}"""
2428 signature = "9d25de966891ab0bc18754faf8d83d0980b44ae330fcc130b41a6cf3daf1f391"
@@ -106,12 +110,98 @@ def test_verify_signature(self, client: Orb) -> None:
106110 secret = secret ,
107111 )
108112
113+ def test_microsecond_precision_issue_fixed (self , client : Orb ) -> None :
114+ """Test that the webhook timestamp parsing issue is fixed for the reported examples."""
115+ import hmac
116+ import hashlib
117+
118+ secret = self .secret
119+
120+ # Test cases from the reported issue - these should all work now
121+ test_cases = [
122+ ("2025-08-08T21:35:11.531998+00:00" , "2025-08-08T21:35:11.445" ),
123+ ("2025-08-08T21:32:02.585239+00:00" , "2025-08-08T21:32:02.497" ),
124+ ("2025-08-08T21:35:42.810490+00:00" , "2025-08-08T21:35:42.660" ),
125+ ]
126+
127+ for _ , (system_time_str , webhook_timestamp ) in enumerate (test_cases , 1 ):
128+ system_time = datetime .fromisoformat (system_time_str )
129+
130+ # Generate the correct signature
131+ to_sign = f"v1:{ webhook_timestamp } :{ self .payload } " .encode ("utf-8" )
132+ signature = hmac .new (secret .encode ("utf-8" ), to_sign , hashlib .sha256 ).hexdigest ()
133+
134+ with time_machine .travel (system_time ):
135+ # This should now work without raising "Webhook timestamp is too new"
136+ client .webhooks .verify_signature (
137+ payload = self .payload ,
138+ headers = {"X-Orb-Timestamp" : webhook_timestamp , "X-Orb-Signature" : f"v1={ signature } " },
139+ secret = secret ,
140+ )
141+
142+ # Also test the unwrap method
143+ result = client .webhooks .unwrap (
144+ payload = self .payload ,
145+ headers = {"X-Orb-Timestamp" : webhook_timestamp , "X-Orb-Signature" : f"v1={ signature } " },
146+ secret = secret ,
147+ )
148+ assert result is not None
149+
150+ def test_timezone_aware_timestamps_still_work (self , client : Orb ) -> None :
151+ """Test that webhook timestamps with explicit timezone info still work."""
152+ import hmac
153+ import hashlib
154+ from datetime import timezone
155+
156+ secret = self .secret
157+
158+ # Test with explicit UTC timezone
159+ system_time = datetime (2025 , 8 , 8 , 21 , 35 , 11 , 531998 , tzinfo = timezone .utc )
160+ webhook_timestamp = "2025-08-08T21:35:11.445+00:00" # Explicit UTC
161+
162+ to_sign = f"v1:{ webhook_timestamp } :{ self .payload } " .encode ("utf-8" )
163+ signature = hmac .new (secret .encode ("utf-8" ), to_sign , hashlib .sha256 ).hexdigest ()
164+
165+ with time_machine .travel (system_time ):
166+ client .webhooks .verify_signature (
167+ payload = self .payload ,
168+ headers = {"X-Orb-Timestamp" : webhook_timestamp , "X-Orb-Signature" : f"v1={ signature } " },
169+ secret = secret ,
170+ )
171+
172+ def test_webhook_timestamp_actually_too_new (self , client : Orb ) -> None :
173+ """Test that webhooks that are genuinely too new are still rejected."""
174+ import hmac
175+ import hashlib
176+ from datetime import timezone
177+
178+ secret = self .secret
179+
180+ # Set system time to be much earlier than webhook timestamp (more than 5 minute tolerance)
181+ system_time = datetime (2025 , 8 , 8 , 21 , 30 , 0 , 0 , tzinfo = timezone .utc )
182+ webhook_timestamp = "2025-08-08T21:36:00.000" # 6 minutes later - should be rejected
183+
184+ to_sign = f"v1:{ webhook_timestamp } :{ self .payload } " .encode ("utf-8" )
185+ signature = hmac .new (secret .encode ("utf-8" ), to_sign , hashlib .sha256 ).hexdigest ()
186+
187+ with time_machine .travel (system_time ):
188+ with pytest .raises (ValueError , match = "Webhook timestamp is too new" ):
189+ client .webhooks .verify_signature (
190+ payload = self .payload ,
191+ headers = {"X-Orb-Timestamp" : webhook_timestamp , "X-Orb-Signature" : f"v1={ signature } " },
192+ secret = secret ,
193+ )
194+
109195
110196class TestAsyncWebhooks :
111197 parametrize = pytest .mark .parametrize ("async_client" , [False , True ], indirect = True , ids = ["loose" , "strict" ])
112198
113199 timestamp = "2024-03-27T15:42:29.551"
114- fake_now = datetime .fromisoformat (timestamp ).astimezone ()
200+ # Fix: Ensure fake_now matches how webhook timestamps are now parsed (UTC assumption)
201+ fake_now_dt = datetime .fromisoformat (timestamp )
202+ if fake_now_dt .tzinfo is None :
203+ fake_now_dt = fake_now_dt .replace (tzinfo = timezone .utc )
204+ fake_now = fake_now_dt .astimezone ()
115205
116206 payload = """{"id": "o4mmewpfNNTnjfZc", "created_at": "2024-03-27T15:42:29+00:00", "type": "resource_event.test", "properties": {"message": "A test webhook from Orb. Happy testing!"}}"""
117207 signature = "9d25de966891ab0bc18754faf8d83d0980b44ae330fcc130b41a6cf3daf1f391"
0 commit comments