Skip to content

Commit aa21aa2

Browse files
committed
feat(webhooks): support Basic Auth credentials and read flags
1 parent ea319f7 commit aa21aa2

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

‎src/lettermint/types.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,14 @@
576576
]
577577
WebhookScope: TypeAlias = Literal["team", "project", "route"]
578578
WebhookDeliveryModeFilter: TypeAlias = Literal["live", "sandbox", "both"]
579+
WebhookBasicAuthData = TypedDict(
580+
"WebhookBasicAuthData",
581+
{
582+
"username": "Required[str]",
583+
"password": "Required[str]",
584+
},
585+
)
586+
579587
StoreWebhookData = TypedDict(
580588
"StoreWebhookData",
581589
{
@@ -589,6 +597,7 @@
589597
"route_ids": "NotRequired[list[str]]",
590598
"route_id": "NotRequired[str | None]",
591599
"delivery_mode_filter": "NotRequired[WebhookDeliveryModeFilter | None]",
600+
"basic_auth": "NotRequired[WebhookBasicAuthData | None]",
592601
},
593602
)
594603

@@ -779,6 +788,7 @@
779788
"route_ids": "NotRequired[list[str]]",
780789
"route_id": "NotRequired[str | None]",
781790
"delivery_mode_filter": "NotRequired[WebhookDeliveryModeFilter]",
791+
"basic_auth": "NotRequired[WebhookBasicAuthData | None]",
782792
},
783793
)
784794

@@ -792,6 +802,7 @@
792802
"route_id": "Required[str | None]",
793803
"name": "Required[str]",
794804
"url": "Required[str]",
805+
"has_basic_auth": "Required[bool]",
795806
"events": "Required[list[str]]",
796807
"enabled": "Required[bool]",
797808
"include_machine_events": "Required[bool]",
@@ -864,6 +875,7 @@
864875
"last_called_at": "Required[str | None]",
865876
"created_at": "Required[str]",
866877
"updated_at": "Required[str]",
878+
"has_basic_auth": "Required[bool]",
867879
},
868880
)
869881

@@ -886,6 +898,7 @@
886898
"created_at": "Required[str]",
887899
"updated_at": "Required[str]",
888900
"delivery_mode_filter": "Required[WebhookDeliveryModeFilter]",
901+
"has_basic_auth": "Required[bool]",
889902
},
890903
)
891904

‎tests/test_webhook_basic_auth.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from typing import get_type_hints
5+
6+
import pytest
7+
import respx
8+
from httpx import Response
9+
10+
from lettermint import AsyncLettermint, HttpRequestError, Lettermint
11+
from lettermint import types as lm_types
12+
13+
14+
@pytest.mark.parametrize(
15+
"state",
16+
[{}, {"basic_auth": {"username": " fixture user ", "password": ""}}, {"basic_auth": None}],
17+
)
18+
@pytest.mark.parametrize("asynchronous", [False, True])
19+
@respx.mock
20+
@pytest.mark.asyncio
21+
async def test_webhook_credential_states_keep_bearer_auth(state: dict, asynchronous: bool) -> None:
22+
create = respx.post("https://api.lettermint.co/v1/webhooks").mock(
23+
return_value=Response(201, json={"data": {"has_basic_auth": True}})
24+
)
25+
update = respx.put("https://api.lettermint.co/v1/webhooks/webhook-id").mock(
26+
return_value=Response(200, json={"data": {"has_basic_auth": True}})
27+
)
28+
payload = {
29+
"name": "Fixture",
30+
"url": "https://example.test/hook",
31+
"events": ["message.sent"],
32+
**state,
33+
}
34+
if asynchronous:
35+
async with AsyncLettermint.api("fixture-token") as api:
36+
assert (await api.webhooks.create(payload))["data"]["has_basic_auth"] is True
37+
assert (await api.webhooks.update("webhook-id", state))["data"][
38+
"has_basic_auth"
39+
] is True
40+
else:
41+
with Lettermint.api("fixture-token") as sync_api:
42+
assert sync_api.webhooks.create(payload)["data"]["has_basic_auth"] is True
43+
assert sync_api.webhooks.update("webhook-id", state)["data"]["has_basic_auth"] is True
44+
for route, expected in [(create, payload), (update, state)]:
45+
request = route.calls.last.request
46+
assert json.loads(request.content) == expected
47+
assert request.headers["authorization"] == "Bearer fixture-token"
48+
assert "x-lettermint-token" not in request.headers
49+
50+
51+
def test_webhook_types_expose_required_read_flag_and_optional_nullable_credentials() -> None:
52+
for model in [lm_types.WebhookData, lm_types.WebhookListData, lm_types.WebhookSecretData]:
53+
assert "Required[bool]" in str(get_type_hints(model, include_extras=True)["has_basic_auth"])
54+
for model in [lm_types.StoreWebhookData, lm_types.UpdateWebhookData]:
55+
assert "NotRequired" in str(get_type_hints(model, include_extras=True)["basic_auth"])
56+
assert "has_basic_auth" not in get_type_hints(model, include_extras=True)
57+
credentials: lm_types.WebhookBasicAuthData = {"username": "fixture", "password": ""}
58+
assert credentials["password"] == ""
59+
60+
61+
@respx.mock
62+
def test_free_plan_sandbox_keeps_403_response() -> None:
63+
body = {
64+
"error": {
65+
"code": "FEATURE_NOT_AVAILABLE",
66+
"message": "Sandbox mode is available only on paid plans.",
67+
}
68+
}
69+
respx.post("https://api.lettermint.co/v1/send").mock(return_value=Response(403, json=body))
70+
with Lettermint.email("fixture-token") as email, pytest.raises(HttpRequestError) as caught:
71+
email.from_("from@example.test").to("to@example.test").subject("Fixture").send()
72+
assert caught.value.status_code == 403
73+
assert caught.value.response_body == body

0 commit comments

Comments
 (0)