Skip to content

Commit 69048d8

Browse files
refactor(webhooks): rename ungzip_payload to gunzip_payload + add golden fixtures (CHA-3071)
Per Tommaso's suggestion, align the gzip helper with the GNU `gunzip` command name. The function was added in this PR and not yet released, so this is a straight rename with no back-compat alias. Adds Tommaso's reference fixtures to the test suite as named cases so future SDKs can sanity-check against the same payloads: aGVsbG93b3JsZA== -> helloworld (base64) H4sIAGrYAWoAA8tIzcnJL88vykkBAK0g6/kKAAAA -> helloworld (base64+gzip) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c59702f commit 69048d8

3 files changed

Lines changed: 28 additions & 15 deletions

File tree

docs/webhooks/webhooks_overview/webhooks_overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ event = webhook.verify_and_parse_sqs(message_body, signature, secret)
177177
event = webhook.verify_and_parse_sns(notification_body, signature, secret)
178178
```
179179

180-
The module also exposes the primitives the composites are built from — `ungzip_payload`, `decode_sqs_payload`, `decode_sns_payload`, `verify_signature` (constant-time HMAC-SHA256), and `parse_event` — for callers that need to run the steps individually.
180+
The module also exposes the primitives the composites are built from — `gunzip_payload`, `decode_sqs_payload`, `decode_sns_payload`, `verify_signature` (constant-time HMAC-SHA256), and `parse_event` — for callers that need to run the steps individually.
181181

182182
All webhook requests contain these headers:
183183

stream_chat/tests/test_webhook_compression.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
* Module-level functions in :mod:`stream_chat.webhook`:
66
7-
* primitives - ``ungzip_payload``, ``decode_sqs_payload``,
7+
* primitives - ``gunzip_payload``, ``decode_sqs_payload``,
88
``decode_sns_payload``, ``verify_signature``, ``parse_event``
99
* composite helpers - ``verify_and_parse_webhook``,
1010
``verify_and_parse_sqs``, ``verify_and_parse_sns``
@@ -32,8 +32,8 @@
3232
GZIP_MAGIC,
3333
decode_sns_payload,
3434
decode_sqs_payload,
35+
gunzip_payload,
3536
parse_event,
36-
ungzip_payload,
3737
verify_and_parse_sns,
3838
verify_and_parse_sqs,
3939
verify_and_parse_webhook,
@@ -66,32 +66,36 @@ def sync_client() -> StreamChat:
6666
return StreamChat(api_key=API_KEY, api_secret=API_SECRET)
6767

6868

69-
class TestUngzipPayload:
69+
class TestGunzipPayload:
7070
def test_passthrough_plain_bytes(self):
71-
assert ungzip_payload(JSON_BODY) == JSON_BODY
71+
assert gunzip_payload(JSON_BODY) == JSON_BODY
7272

7373
def test_passthrough_str_input(self):
74-
assert ungzip_payload(JSON_BODY.decode("utf-8")) == JSON_BODY
74+
assert gunzip_payload(JSON_BODY.decode("utf-8")) == JSON_BODY
7575

7676
def test_inflates_gzip_bytes(self):
77-
assert ungzip_payload(_gzip(JSON_BODY)) == JSON_BODY
77+
assert gunzip_payload(_gzip(JSON_BODY)) == JSON_BODY
7878

7979
def test_returns_bytes(self):
80-
assert isinstance(ungzip_payload(JSON_BODY), bytes)
81-
assert isinstance(ungzip_payload(_gzip(JSON_BODY)), bytes)
80+
assert isinstance(gunzip_payload(JSON_BODY), bytes)
81+
assert isinstance(gunzip_payload(_gzip(JSON_BODY)), bytes)
8282

8383
def test_empty_input(self):
84-
assert ungzip_payload(b"") == b""
84+
assert gunzip_payload(b"") == b""
8585

8686
def test_short_input_below_magic_length(self):
87-
assert ungzip_payload(b"ab") == b"ab"
87+
assert gunzip_payload(b"ab") == b"ab"
8888

8989
def test_truncated_gzip_with_magic_raises(self):
9090
bad = GZIP_MAGIC + b"\x00\x00\x00"
9191
with pytest.raises(WebhookSignatureError) as exc_info:
92-
ungzip_payload(bad)
92+
gunzip_payload(bad)
9393
assert "decompress" in str(exc_info.value).lower()
9494

95+
def test_decompresses_helloworld_fixture(self):
96+
gz_bytes = base64.b64decode("H4sIAGrYAWoAA8tIzcnJL88vykkBAK0g6/kKAAAA")
97+
assert gunzip_payload(gz_bytes) == b"helloworld"
98+
9599

96100
class TestDecodeSqsPayload:
97101
def test_base64_only_no_compression(self):
@@ -114,6 +118,15 @@ def test_invalid_base64_raises(self):
114118
decode_sqs_payload("!!!not-valid-base64!!!")
115119
assert "base64" in str(exc_info.value).lower()
116120

121+
def test_decodes_helloworld_base64_fixture(self):
122+
assert decode_sqs_payload("aGVsbG93b3JsZA==") == b"helloworld"
123+
124+
def test_decodes_helloworld_base64_gzip_fixture(self):
125+
assert (
126+
decode_sqs_payload("H4sIAGrYAWoAA8tIzcnJL88vykkBAK0g6/kKAAAA")
127+
== b"helloworld"
128+
)
129+
117130

118131
def _sns_envelope(inner_message: str) -> str:
119132
return json.dumps(

stream_chat/webhook.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def _to_bytes(body: _BytesLike) -> bytes:
3636
raise TypeError(f"webhook body must be bytes or str, got {type(body).__name__}")
3737

3838

39-
def ungzip_payload(body: _BytesLike) -> bytes:
39+
def gunzip_payload(body: _BytesLike) -> bytes:
4040
"""Return ``body`` unchanged unless it starts with the gzip magic
4141
(``1f 8b``, per RFC 1952), in which case the gzip stream is decompressed.
4242
@@ -67,7 +67,7 @@ def decode_sqs_payload(body: _BytesLike) -> bytes:
6767
decoded = base64.b64decode(raw, validate=True)
6868
except ValueError as exc:
6969
raise WebhookSignatureError(f"failed to base64-decode payload: {exc}")
70-
return ungzip_payload(decoded)
70+
return gunzip_payload(decoded)
7171

7272

7373
def decode_sns_payload(notification_body: _BytesLike) -> bytes:
@@ -168,7 +168,7 @@ def verify_and_parse_webhook(
168168
:param secret: the app's API secret
169169
:raises WebhookSignatureError: on signature mismatch or decode error
170170
"""
171-
inflated = ungzip_payload(body)
171+
inflated = gunzip_payload(body)
172172
return _verify_and_parse(inflated, signature, secret)
173173

174174

0 commit comments

Comments
 (0)