Skip to content

Commit adb3003

Browse files
jacalataclaude
andcommitted
fix: accept both event names and source names for webhooks
Fixes #1597 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 387d043 commit adb3003

3 files changed

Lines changed: 102 additions & 3 deletions

File tree

‎tableauserverclient/models/webhook_item.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,17 @@ def id(self) -> str | None:
7272
@property
7373
def event(self) -> str | None:
7474
if self._event:
75-
return self._event.replace("webhook-source-event-", "")
75+
return self._event.removeprefix("webhook-source-event-")
7676
return None
7777

7878
@event.setter
79-
def event(self, value: str) -> None:
80-
self._event = f"webhook-source-event-{value}"
79+
def event(self, value: str | None) -> None:
80+
if value is None:
81+
self._event = None
82+
elif value.startswith("webhook-source-event-") or value.startswith("webhook-event-"):
83+
self._event = value
84+
else:
85+
self._event = f"webhook-source-event-{value}"
8186

8287
@classmethod
8388
def from_response(cls: type["WebhookItem"], resp: bytes, ns) -> list["WebhookItem"]:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd">
3+
<webhooks>
4+
<webhook id="webhook-id-2" name="webhook-name-2">
5+
<webhook-source>
6+
<webhook-event-user-promoted-admin />
7+
</webhook-source>
8+
<webhook-destination>
9+
<webhook-destination-http method="POST" url="https://example.com/hook"/>
10+
</webhook-destination>
11+
<owner id="webhook_owner_luid" name="webhook_owner_name"/>
12+
</webhook>
13+
</webhooks>
14+
</tsResponse>

‎test/test_webhook.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
TEST_ASSET_DIR = Path(__file__).parent / "assets"
1111

1212
GET_XML = TEST_ASSET_DIR / "webhook_get.xml"
13+
GET_NEW_EVENT_XML = TEST_ASSET_DIR / "webhook_get_new_event.xml"
1314
CREATE_XML = TEST_ASSET_DIR / "webhook_create.xml"
1415
CREATE_REQUEST_XML = TEST_ASSET_DIR / "webhook_create_request.xml"
1516

@@ -87,3 +88,82 @@ def test_request_factory():
8788
webhook_request_actual = "{}\n".format(RequestFactory.Webhook.create_req(webhook_item).decode("utf-8"))
8889
# windows does /r/n for linebreaks, remove the extra char if it is there
8990
assert webhook_request_expected.replace("\r", "") == webhook_request_actual
91+
92+
93+
def test_event_setter_none():
94+
"""Setting event to None should store None without crashing."""
95+
item = WebhookItem()
96+
item.event = "datasource-updated"
97+
assert item.event == "datasource-updated"
98+
item.event = None
99+
assert item._event is None
100+
assert item.event is None
101+
102+
103+
def test_event_setter_short_name():
104+
"""Short event names should be stored with the webhook-source-event- prefix."""
105+
item = WebhookItem()
106+
item.event = "datasource-updated"
107+
assert item._event == "webhook-source-event-datasource-updated"
108+
assert item.event == "datasource-updated"
109+
110+
111+
def test_event_setter_full_source_name():
112+
"""Full webhook-source-event- names should be accepted and stored as-is."""
113+
item = WebhookItem()
114+
item.event = "webhook-source-event-datasource-updated"
115+
assert item._event == "webhook-source-event-datasource-updated"
116+
assert item.event == "datasource-updated"
117+
118+
119+
def test_event_setter_new_style_event_name():
120+
"""New-style event names (webhook-event-*) should be stored as-is and not mangled."""
121+
item = WebhookItem()
122+
item.event = "webhook-event-user-promoted-admin"
123+
assert item._event == "webhook-event-user-promoted-admin"
124+
assert item.event == "webhook-event-user-promoted-admin"
125+
126+
127+
def test_get_new_style_event(server: TSC.Server) -> None:
128+
"""Webhooks with new-style event names (webhook-event-*) should parse correctly."""
129+
response_xml = GET_NEW_EVENT_XML.read_text()
130+
with requests_mock.mock() as m:
131+
m.get(server.webhooks.baseurl, text=response_xml)
132+
webhooks, _ = server.webhooks.get()
133+
assert len(webhooks) == 1
134+
webhook = webhooks[0]
135+
136+
assert webhook.id == "webhook-id-2"
137+
assert webhook.name == "webhook-name-2"
138+
assert webhook.url == "https://example.com/hook"
139+
# New-style event name should not have the webhook-source-event- prefix stripped
140+
assert webhook.event == "webhook-event-user-promoted-admin"
141+
assert webhook.owner_id == "webhook_owner_luid"
142+
143+
144+
def test_create_with_short_event_name(server: TSC.Server) -> None:
145+
"""Creating a webhook with a short event name (e.g. datasource-created) should work."""
146+
response_xml = CREATE_XML.read_text()
147+
with requests_mock.mock() as m:
148+
m.post(server.webhooks.baseurl, text=response_xml)
149+
webhook_model = TSC.WebhookItem()
150+
webhook_model.name = "Test Webhook"
151+
webhook_model.url = "https://ifttt.com/maker-url"
152+
webhook_model.event = "datasource-created"
153+
154+
new_webhook = server.webhooks.create(webhook_model)
155+
assert new_webhook.id is not None
156+
157+
158+
def test_create_with_source_event_name(server: TSC.Server) -> None:
159+
"""Creating a webhook with a full webhook-source-event-* name should work."""
160+
response_xml = CREATE_XML.read_text()
161+
with requests_mock.mock() as m:
162+
m.post(server.webhooks.baseurl, text=response_xml)
163+
webhook_model = TSC.WebhookItem()
164+
webhook_model.name = "Test Webhook"
165+
webhook_model.url = "https://ifttt.com/maker-url"
166+
webhook_model.event = "webhook-source-event-datasource-created"
167+
168+
new_webhook = server.webhooks.create(webhook_model)
169+
assert new_webhook.id is not None

0 commit comments

Comments
 (0)