hookbase 2.9.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hookbase-2.9.0 → hookbase-3.0.0}/PKG-INFO +1 -1
- hookbase-3.0.0/src/hookbase/_version.py +1 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/sources.py +78 -33
- hookbase-3.0.0/tests/resources/test_source_fields.py +105 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/resources/test_sources.py +6 -3
- hookbase-2.9.0/src/hookbase/_version.py +0 -1
- {hookbase-2.9.0 → hookbase-3.0.0}/.github/workflows/python-publish.yml +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/.gitignore +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/LICENSE +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/README.md +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/examples/async_usage.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/examples/basic_inbound.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/examples/error_handling.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/examples/send_webhooks.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/examples/verify_webhooks.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/pyproject.toml +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/__init__.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/_client.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/_constants.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/_pagination.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/client.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/errors.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/__init__.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/_base.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/analytics.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/api_keys.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/applications.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/common.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/cron_jobs.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/deliveries.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/destinations.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/dlq.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/endpoints.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/event_types.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/events.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/filters.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/messages.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/organizations.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/portal_tokens.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/routes.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/schemas.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/subscriptions.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/transforms.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/models/tunnels.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/py.typed +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/__init__.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/_base.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/analytics.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/api_keys.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/applications.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/cron_jobs.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/deliveries.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/destinations.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/dlq.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/endpoints.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/event_types.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/events.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/filters.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/message_log.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/messages.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/organizations.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/portal_tokens.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/routes.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/schemas.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/sources.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/subscriptions.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/transforms.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/resources/tunnels.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/src/hookbase/webhook.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/__init__.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/conftest.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/resources/__init__.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/resources/test_applications.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/resources/test_cron_jobs.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/resources/test_destinations.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/resources/test_dlq.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/resources/test_messages.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/test_client.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/test_errors.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/test_pagination.py +0 -0
- {hookbase-2.9.0 → hookbase-3.0.0}/tests/test_webhook.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.0.0"
|
|
@@ -21,9 +21,10 @@ from ._base import HookbaseModel
|
|
|
21
21
|
#
|
|
22
22
|
# "svix" is an alias of "standard-webhooks"; both resolve to the same scheme server-side.
|
|
23
23
|
SourceProvider = Literal[
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
24
|
+
"airtable", "asana", "bitbucket", "calendly", "custom", "generic",
|
|
25
|
+
"github", "gitlab", "heroku", "intercom", "lemonsqueezy", "notion",
|
|
26
|
+
"paddle", "razorpay", "sentry", "shopify", "slack", "standard-webhooks",
|
|
27
|
+
"stripe", "svix", "twilio", "typeform", "workos", "zoom",
|
|
27
28
|
]
|
|
28
29
|
# Corrected alongside SourceProvider: "header" and "event_id" were never accepted, and the three
|
|
29
30
|
# values the API actually defaults to and documents were missing. "auto" is the default.
|
|
@@ -35,48 +36,77 @@ IngestMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class Source(HookbaseModel):
|
|
39
|
+
"""A source as the API returns it.
|
|
40
|
+
|
|
41
|
+
The signing secret is not included. Responses carry ``has_signing_secret`` and the last four
|
|
42
|
+
characters; the full value comes back exactly twice, from :meth:`Sources.create` (which
|
|
43
|
+
returns a :class:`SourceWithSecret`) and from :meth:`Sources.reveal_secret`.
|
|
44
|
+
"""
|
|
45
|
+
|
|
38
46
|
id: str
|
|
39
47
|
organization_id: str | None = None
|
|
40
48
|
name: str
|
|
41
49
|
slug: str
|
|
50
|
+
provider: SourceProvider | None = None
|
|
42
51
|
description: str | None = None
|
|
43
|
-
|
|
52
|
+
#: Whether a signing secret is set. The secret itself is not returned here.
|
|
53
|
+
has_signing_secret: bool = False
|
|
54
|
+
#: Last four characters of the signing secret, prefixed with "...", or None if unset.
|
|
55
|
+
signing_secret_last4: str | None = None
|
|
56
|
+
#: Reject events whose signature fails verification. Defaults to False, in which case a
|
|
57
|
+
#: failing event is flagged with signature_valid=False and delivered anyway.
|
|
58
|
+
reject_invalid_signatures: bool = False
|
|
59
|
+
rate_limit_per_minute: int | None = None
|
|
44
60
|
is_active: bool = True
|
|
45
|
-
|
|
46
|
-
ingest_url: str | None = None
|
|
47
|
-
verify_signature: bool = False
|
|
48
|
-
dedup_strategy: str | None = "none"
|
|
49
|
-
dedup_window: int | None = None
|
|
50
|
-
dedup_header_name: str | None = None
|
|
61
|
+
custom_domain_id: str | None = None
|
|
51
62
|
ip_filter_mode: IpFilterMode = "none"
|
|
52
|
-
ip_allowlist: list[str]
|
|
53
|
-
ip_denylist: list[str]
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
ip_allowlist: list[str] = []
|
|
64
|
+
ip_denylist: list[str] = []
|
|
65
|
+
#: JSONPath expressions whose values are encrypted at rest.
|
|
66
|
+
encrypt_fields: list[str] = []
|
|
67
|
+
#: JSONPath expressions whose values are masked in stored payloads.
|
|
68
|
+
mask_fields: list[str] = []
|
|
69
|
+
dedup_enabled: bool = False
|
|
70
|
+
dedup_strategy: DedupStrategy = "auto"
|
|
71
|
+
dedup_window_hours: int = 24
|
|
72
|
+
dedup_custom_header: str | None = None
|
|
56
73
|
transient_mode: bool = False
|
|
57
74
|
#: HTTP verbs the ingest endpoint accepts. Empty list means any method.
|
|
58
75
|
allowed_methods: list[str] = []
|
|
59
76
|
event_count: int = 0
|
|
60
|
-
|
|
77
|
+
route_count: int = 0
|
|
78
|
+
#: Returned by get() and create(). Not included in list() responses.
|
|
79
|
+
ingest_url: str | None = None
|
|
61
80
|
created_at: str = ""
|
|
62
81
|
updated_at: str = ""
|
|
63
82
|
|
|
64
|
-
@field_validator(
|
|
83
|
+
@field_validator(
|
|
84
|
+
"is_active",
|
|
85
|
+
"reject_invalid_signatures",
|
|
86
|
+
"has_signing_secret",
|
|
87
|
+
"dedup_enabled",
|
|
88
|
+
"transient_mode",
|
|
89
|
+
mode="before",
|
|
90
|
+
)
|
|
65
91
|
@classmethod
|
|
66
92
|
def parse_bool_from_int(cls, v: Any) -> Any:
|
|
67
93
|
if isinstance(v, int):
|
|
68
94
|
return bool(v)
|
|
69
95
|
return v
|
|
70
96
|
|
|
71
|
-
@field_validator(
|
|
97
|
+
@field_validator(
|
|
98
|
+
"ip_allowlist", "ip_denylist", "encrypt_fields", "mask_fields", mode="before"
|
|
99
|
+
)
|
|
72
100
|
@classmethod
|
|
73
101
|
def parse_json_list(cls, v: Any) -> Any:
|
|
102
|
+
if v is None:
|
|
103
|
+
return []
|
|
74
104
|
if isinstance(v, str):
|
|
75
105
|
try:
|
|
76
106
|
parsed = json.loads(v)
|
|
77
|
-
return parsed if isinstance(parsed, list) else
|
|
107
|
+
return parsed if isinstance(parsed, list) else []
|
|
78
108
|
except (json.JSONDecodeError, ValueError):
|
|
79
|
-
return
|
|
109
|
+
return []
|
|
80
110
|
return v
|
|
81
111
|
|
|
82
112
|
@field_validator("allowed_methods", mode="before")
|
|
@@ -95,23 +125,34 @@ class Source(HookbaseModel):
|
|
|
95
125
|
|
|
96
126
|
|
|
97
127
|
class SourceWithSecret(Source):
|
|
128
|
+
"""What create() returns: a source plus the one look at its signing secret you get."""
|
|
129
|
+
|
|
98
130
|
signing_secret: str = ""
|
|
99
131
|
|
|
100
132
|
|
|
101
133
|
class CreateSourceParams(HookbaseModel):
|
|
102
134
|
name: str
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
#: Required: it forms the ingest URL, /ingest/<org>/<slug>, and cannot be changed later.
|
|
136
|
+
slug: str
|
|
105
137
|
provider: SourceProvider | None = None
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
description: str | None = None
|
|
139
|
+
#: Supply your own secret to match what the provider is already configured with. Omit it and
|
|
140
|
+
#: the API generates one, returned once on the created source.
|
|
141
|
+
signing_secret: str | None = None
|
|
142
|
+
#: Reject events whose signature fails verification. Defaults to False.
|
|
143
|
+
reject_invalid_signatures: bool | None = None
|
|
144
|
+
rate_limit_per_minute: int | None = None
|
|
110
145
|
ip_filter_mode: IpFilterMode | None = None
|
|
111
146
|
ip_allowlist: list[str] | None = None
|
|
112
147
|
ip_denylist: list[str] | None = None
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
encrypt_fields: list[str] | None = None
|
|
149
|
+
mask_fields: list[str] | None = None
|
|
150
|
+
dedup_enabled: bool | None = None
|
|
151
|
+
dedup_strategy: DedupStrategy | None = None
|
|
152
|
+
#: Deduplication window in hours, 1 to 168.
|
|
153
|
+
dedup_window_hours: int | None = None
|
|
154
|
+
#: Header to deduplicate on when dedup_strategy is "idempotency_key".
|
|
155
|
+
dedup_custom_header: str | None = None
|
|
115
156
|
transient_mode: bool | None = None
|
|
116
157
|
#: Restrict the ingest endpoint to these verbs. None/[] accepts any method.
|
|
117
158
|
allowed_methods: list[IngestMethod] | None = None
|
|
@@ -120,16 +161,20 @@ class CreateSourceParams(HookbaseModel):
|
|
|
120
161
|
class UpdateSourceParams(HookbaseModel):
|
|
121
162
|
name: str | None = None
|
|
122
163
|
description: str | None = None
|
|
164
|
+
provider: SourceProvider | None = None
|
|
123
165
|
is_active: bool | None = None
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
dedup_header_name: str | None = None
|
|
166
|
+
signing_secret: str | None = None
|
|
167
|
+
reject_invalid_signatures: bool | None = None
|
|
168
|
+
rate_limit_per_minute: int | None = None
|
|
128
169
|
ip_filter_mode: IpFilterMode | None = None
|
|
129
170
|
ip_allowlist: list[str] | None = None
|
|
130
171
|
ip_denylist: list[str] | None = None
|
|
131
|
-
|
|
132
|
-
|
|
172
|
+
encrypt_fields: list[str] | None = None
|
|
173
|
+
mask_fields: list[str] | None = None
|
|
174
|
+
dedup_enabled: bool | None = None
|
|
175
|
+
dedup_strategy: DedupStrategy | None = None
|
|
176
|
+
dedup_window_hours: int | None = None
|
|
177
|
+
dedup_custom_header: str | None = None
|
|
133
178
|
transient_mode: bool | None = None
|
|
134
179
|
#: Restrict the ingest endpoint to these verbs. Pass [] to accept any method again.
|
|
135
180
|
allowed_methods: list[IngestMethod] | None = None
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""The field names this SDK puts on the wire, as POST/PATCH /api/sources accept them.
|
|
2
|
+
|
|
3
|
+
These lists exist because they were wrong for months and nothing said so. The models named
|
|
4
|
+
``verify_signature``, ``dedup_window``, ``dedup_header_name``, ``rate_limit`` and
|
|
5
|
+
``rate_limit_window``, which the camelCase alias generator sent as ``verifySignature`` and
|
|
6
|
+
friends. The API's create schema was a plain zod object, which strips unknown keys rather than
|
|
7
|
+
refusing them, so every one of those calls returned 201 having quietly dropped the setting: a
|
|
8
|
+
source created with ``verify_signature=True`` came back with verification off and raised no error.
|
|
9
|
+
|
|
10
|
+
The create schema is strict now, so a stale name is a 400 rather than a silent drop — which is why
|
|
11
|
+
a rename here has to be deliberate.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from pydantic import ValidationError
|
|
18
|
+
|
|
19
|
+
from hookbase.models.sources import (
|
|
20
|
+
CreateSourceParams,
|
|
21
|
+
Source,
|
|
22
|
+
SourceWithSecret,
|
|
23
|
+
UpdateSourceParams,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
CREATE_FIELDS = {
|
|
27
|
+
"name", "slug", "provider", "description", "signingSecret",
|
|
28
|
+
"rejectInvalidSignatures", "rateLimitPerMinute", "ipFilterMode", "ipAllowlist",
|
|
29
|
+
"ipDenylist", "encryptFields", "maskFields", "dedupEnabled", "dedupStrategy",
|
|
30
|
+
"dedupWindowHours", "dedupCustomHeader", "transientMode", "allowedMethods",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
UPDATE_FIELDS = (CREATE_FIELDS - {"slug"}) | {"isActive"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _aliases(model: type) -> set[str]:
|
|
37
|
+
return {f.alias or name for name, f in model.model_fields.items()}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_create_params_send_exactly_the_accepted_fields() -> None:
|
|
41
|
+
assert _aliases(CreateSourceParams) == CREATE_FIELDS
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_update_params_send_exactly_the_accepted_fields() -> None:
|
|
45
|
+
assert _aliases(UpdateSourceParams) == UPDATE_FIELDS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_create_and_update_differ_only_by_slug_and_is_active() -> None:
|
|
49
|
+
create, update = _aliases(CreateSourceParams), _aliases(UpdateSourceParams)
|
|
50
|
+
assert create - update == {"slug"}
|
|
51
|
+
assert update - create == {"isActive"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_slug_is_required_on_create() -> None:
|
|
55
|
+
"""slug forms the ingest URL and cannot be changed afterwards, so create demands it."""
|
|
56
|
+
with pytest.raises(ValidationError, match="slug"):
|
|
57
|
+
CreateSourceParams(name="No slug")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_a_full_create_serializes_every_field_under_its_wire_name() -> None:
|
|
61
|
+
params = CreateSourceParams(
|
|
62
|
+
name="Stripe Production",
|
|
63
|
+
slug="stripe",
|
|
64
|
+
provider="stripe",
|
|
65
|
+
description="Live charge events",
|
|
66
|
+
signing_secret="whsec_abc",
|
|
67
|
+
reject_invalid_signatures=True,
|
|
68
|
+
rate_limit_per_minute=600,
|
|
69
|
+
ip_filter_mode="allowlist",
|
|
70
|
+
ip_allowlist=["203.0.113.0/24"],
|
|
71
|
+
ip_denylist=[],
|
|
72
|
+
encrypt_fields=["$.data.object.customer_email"],
|
|
73
|
+
mask_fields=["$.data.object.last4"],
|
|
74
|
+
dedup_enabled=True,
|
|
75
|
+
dedup_strategy="provider_id",
|
|
76
|
+
dedup_window_hours=24,
|
|
77
|
+
dedup_custom_header="X-Idempotency-Key",
|
|
78
|
+
transient_mode=False,
|
|
79
|
+
allowed_methods=["POST"],
|
|
80
|
+
)
|
|
81
|
+
assert set(params.model_dump(by_alias=True, exclude_none=True)) == CREATE_FIELDS
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_a_listed_source_carries_the_masked_pair_not_the_secret() -> None:
|
|
85
|
+
src = Source(
|
|
86
|
+
id="src_1", name="Stripe", slug="stripe",
|
|
87
|
+
hasSigningSecret=True, signingSecretLast4="...c123",
|
|
88
|
+
rejectInvalidSignatures=True, dedupWindowHours=24, routeCount=3,
|
|
89
|
+
)
|
|
90
|
+
assert src.has_signing_secret is True
|
|
91
|
+
assert src.signing_secret_last4 == "...c123"
|
|
92
|
+
assert src.reject_invalid_signatures is True
|
|
93
|
+
assert src.dedup_window_hours == 24
|
|
94
|
+
assert src.route_count == 3
|
|
95
|
+
assert not hasattr(src, "signing_secret")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_a_created_source_carries_the_secret_and_the_ingest_url() -> None:
|
|
99
|
+
created = SourceWithSecret(
|
|
100
|
+
id="src_1", name="Stripe", slug="stripe",
|
|
101
|
+
signingSecret="whsec_abc",
|
|
102
|
+
ingestUrl="https://api.hookbase.app/ingest/acme/stripe",
|
|
103
|
+
)
|
|
104
|
+
assert created.signing_secret == "whsec_abc"
|
|
105
|
+
assert created.ingest_url is not None and "/ingest/" in created.ingest_url
|
|
@@ -30,10 +30,13 @@ SOURCE_DATA = {
|
|
|
30
30
|
"slug": "github",
|
|
31
31
|
"provider": "github",
|
|
32
32
|
"isActive": True,
|
|
33
|
-
"
|
|
33
|
+
"hasSigningSecret": True,
|
|
34
|
+
"signingSecretLast4": "...c123",
|
|
35
|
+
"rejectInvalidSignatures": True,
|
|
34
36
|
"dedupStrategy": "none",
|
|
35
37
|
"ipFilterMode": "none",
|
|
36
38
|
"eventCount": 42,
|
|
39
|
+
"routeCount": 3,
|
|
37
40
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
38
41
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
39
42
|
}
|
|
@@ -158,7 +161,7 @@ def test_create_source_with_transient_mode(mock_api, client):
|
|
|
158
161
|
|
|
159
162
|
|
|
160
163
|
def test_create_source_params_serialization():
|
|
161
|
-
params = CreateSourceParams(name="test", transient_mode=True)
|
|
164
|
+
params = CreateSourceParams(name="test", slug="test", transient_mode=True)
|
|
162
165
|
dumped = params.model_dump(by_alias=True, exclude_none=True)
|
|
163
166
|
assert dumped["transientMode"] is True
|
|
164
167
|
|
|
@@ -227,7 +230,7 @@ def test_source_allowed_methods_malformed_fails_open():
|
|
|
227
230
|
|
|
228
231
|
|
|
229
232
|
def test_create_source_params_serializes_allowed_methods():
|
|
230
|
-
params = CreateSourceParams(name="s", allowed_methods=["GET", "POST"])
|
|
233
|
+
params = CreateSourceParams(name="s", slug="s", allowed_methods=["GET", "POST"])
|
|
231
234
|
assert params.model_dump(by_alias=True, exclude_none=True)["allowedMethods"] == ["GET", "POST"]
|
|
232
235
|
|
|
233
236
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.9.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|