Skip to content

Commit aea397a

Browse files
authored
Ensure spec compatibility - fix 2 edge cases (#275)
* fix(event): validate attributes for null or empty values Ensure required attributes ('id', 'source', and 'type') are neither null nor empty across v03 and v1 event versions. Includes stricter validation and new test cases. Signed-off-by: Yurii Serhiichuk <savik.ne@gmail.com> * fix(event): remove max length restriction for extension attributes Remove the 20-character maximum length restriction for extension attribute names in both v03 and v1 event versions. Update validation error messages and add tests to ensure compatibility with longer attribute names. Signed-off-by: Yurii Serhiichuk <savik.ne@gmail.com> * chore(cloudevents): bump version to 2.0.0-alpha5 Signed-off-by: Yurii Serhiichuk <savik.ne@gmail.com> * chore(CHANGELOG): update for 2.0.0-alpha5 with PR #275 Signed-off-by: Yurii Serhiichuk <savik.ne@gmail.com> --------- Signed-off-by: Yurii Serhiichuk <savik.ne@gmail.com>
1 parent 9a670f7 commit aea397a

6 files changed

Lines changed: 324 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [2.0.0.alpha5]
10+
11+
### Changed
12+
13+
- Improved spec compatibility of attributes processing. ([#275])
14+
915
## [2.0.0.alpha4]
1016

1117
### Changed
@@ -317,3 +323,4 @@ CloudEvents v2 is a rewrite with ongoing development ([#271])
317323
[#249]: https://github.com/cloudevents/sdk-python/pull/249
318324
[#271]: https://github.com/cloudevents/sdk-python/pull/271
319325
[#273]: https://github.com/cloudevents/sdk-python/pull/273
326+
[#275]: https://github.com/cloudevents/sdk-python/pull/275

src/cloudevents/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15-
__version__ = "2.0.0-alpha4"
15+
__version__ = "2.0.0-alpha5"

src/cloudevents/core/v03/event.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ def _validate_required_attributes(
9999

100100
if "id" not in attributes:
101101
errors["id"].append(MissingRequiredAttributeError(attribute_name="id"))
102-
if attributes.get("id") is None:
102+
if not attributes.get("id"):
103103
errors["id"].append(
104104
InvalidAttributeValueError(
105-
attribute_name="id", msg="Attribute 'id' must not be None"
105+
attribute_name="id", msg="Attribute 'id' must not be None or empty"
106106
)
107107
)
108108
if not isinstance(attributes.get("id"), str):
@@ -114,13 +114,27 @@ def _validate_required_attributes(
114114
errors["source"].append(
115115
MissingRequiredAttributeError(attribute_name="source")
116116
)
117+
if not attributes.get("source"):
118+
errors["source"].append(
119+
InvalidAttributeValueError(
120+
attribute_name="source",
121+
msg="Attribute 'source' must not be None or empty",
122+
)
123+
)
117124
if not isinstance(attributes.get("source"), str):
118125
errors["source"].append(
119126
InvalidAttributeTypeError(attribute_name="source", expected_type=str)
120127
)
121128

122129
if "type" not in attributes:
123130
errors["type"].append(MissingRequiredAttributeError(attribute_name="type"))
131+
if not attributes.get("type"):
132+
errors["type"].append(
133+
InvalidAttributeValueError(
134+
attribute_name="type",
135+
msg="Attribute 'type' must not be None or empty",
136+
)
137+
)
124138
if not isinstance(attributes.get("type"), str):
125139
errors["type"].append(
126140
InvalidAttributeTypeError(attribute_name="type", expected_type=str)
@@ -253,11 +267,11 @@ def _validate_extension_attributes(
253267
msg="Extension attribute 'data' is reserved and must not be used",
254268
)
255269
)
256-
if not (1 <= len(extension_attribute) <= 20):
270+
if not (1 <= len(extension_attribute)):
257271
errors[extension_attribute].append(
258272
CustomExtensionAttributeError(
259273
attribute_name=extension_attribute,
260-
msg=f"Extension attribute '{extension_attribute}' should be between 1 and 20 characters long",
274+
msg=f"Extension attribute name must be at least 1 character long but was '{extension_attribute}'",
261275
)
262276
)
263277
if not re.match(r"^[a-z0-9]+$", extension_attribute):

src/cloudevents/core/v1/event.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,10 @@ def _validate_required_attributes(
9898

9999
if "id" not in attributes:
100100
errors["id"].append(MissingRequiredAttributeError(attribute_name="id"))
101-
if attributes.get("id") is None:
101+
if not attributes.get("id"):
102102
errors["id"].append(
103103
InvalidAttributeValueError(
104-
attribute_name="id", msg="Attribute 'id' must not be None"
104+
attribute_name="id", msg="Attribute 'id' must not be None or empty"
105105
)
106106
)
107107
if not isinstance(attributes.get("id"), str):
@@ -113,13 +113,27 @@ def _validate_required_attributes(
113113
errors["source"].append(
114114
MissingRequiredAttributeError(attribute_name="source")
115115
)
116+
if not attributes.get("source"):
117+
errors["source"].append(
118+
InvalidAttributeValueError(
119+
attribute_name="source",
120+
msg="Attribute 'source' must not be None or empty",
121+
)
122+
)
116123
if not isinstance(attributes.get("source"), str):
117124
errors["source"].append(
118125
InvalidAttributeTypeError(attribute_name="source", expected_type=str)
119126
)
120127

121128
if "type" not in attributes:
122129
errors["type"].append(MissingRequiredAttributeError(attribute_name="type"))
130+
if not attributes.get("type"):
131+
errors["type"].append(
132+
InvalidAttributeValueError(
133+
attribute_name="type",
134+
msg="Attribute 'type' must not be None or empty",
135+
)
136+
)
123137
if not isinstance(attributes.get("type"), str):
124138
errors["type"].append(
125139
InvalidAttributeTypeError(attribute_name="type", expected_type=str)
@@ -238,11 +252,11 @@ def _validate_extension_attributes(
238252
msg="Extension attribute 'data' is reserved and must not be used",
239253
)
240254
)
241-
if not (1 <= len(extension_attribute) <= 20):
255+
if not (1 <= len(extension_attribute)):
242256
errors[extension_attribute].append(
243257
CustomExtensionAttributeError(
244258
attribute_name=extension_attribute,
245-
msg=f"Extension attribute '{extension_attribute}' should be between 1 and 20 characters long",
259+
msg=f"Extension attribute name must be at least 1 character long but was '{extension_attribute}'",
246260
)
247261
)
248262
if not re.match(r"^[a-z0-9]+$", extension_attribute):

tests/test_core/test_v03/test_event.py

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,22 @@ def test_missing_required_attributes() -> None:
3434
expected_errors = {
3535
"source": [
3636
str(MissingRequiredAttributeError("source")),
37+
str(
38+
InvalidAttributeValueError(
39+
attribute_name="source",
40+
msg="Attribute 'source' must not be None or empty",
41+
)
42+
),
3743
str(InvalidAttributeTypeError("source", str)),
3844
],
3945
"type": [
4046
str(MissingRequiredAttributeError("type")),
47+
str(
48+
InvalidAttributeValueError(
49+
attribute_name="type",
50+
msg="Attribute 'type' must not be None or empty",
51+
)
52+
),
4153
str(InvalidAttributeTypeError("type", str)),
4254
],
4355
}
@@ -277,40 +289,139 @@ def test_schemaurl_validation(schemaurl: Any, expected_error: dict) -> None:
277289

278290

279291
@pytest.mark.parametrize(
280-
"extension_name,expected_error",
292+
"attributes,expected_errors",
281293
[
282294
(
283-
"",
295+
{"id": "", "source": "/", "type": "test"},
284296
{
285-
"": [
297+
"id": [
286298
str(
287-
CustomExtensionAttributeError(
288-
"",
289-
"Extension attribute '' should be between 1 and 20 characters long",
299+
InvalidAttributeValueError(
300+
attribute_name="id",
301+
msg="Attribute 'id' must not be None or empty",
302+
)
303+
)
304+
]
305+
},
306+
),
307+
(
308+
{"id": None, "source": "/", "type": "test"},
309+
{
310+
"id": [
311+
str(
312+
InvalidAttributeValueError(
313+
attribute_name="id",
314+
msg="Attribute 'id' must not be None or empty",
290315
)
291316
),
292317
str(
293-
CustomExtensionAttributeError(
294-
"",
295-
"Extension attribute '' should only contain lowercase letters and numbers",
318+
InvalidAttributeTypeError(
319+
attribute_name="id", expected_type=str
296320
)
297321
),
298322
]
299323
},
300324
),
301325
(
302-
"thisisaverylongextension",
326+
{"id": "1", "source": "", "type": "test"},
303327
{
304-
"thisisaverylongextension": [
328+
"source": [
305329
str(
306-
CustomExtensionAttributeError(
307-
"thisisaverylongextension",
308-
"Extension attribute 'thisisaverylongextension' should be between 1 and 20 characters long",
330+
InvalidAttributeValueError(
331+
attribute_name="source",
332+
msg="Attribute 'source' must not be None or empty",
333+
)
334+
)
335+
]
336+
},
337+
),
338+
(
339+
{"id": "1", "source": None, "type": "test"},
340+
{
341+
"source": [
342+
str(
343+
InvalidAttributeValueError(
344+
attribute_name="source",
345+
msg="Attribute 'source' must not be None or empty",
346+
)
347+
),
348+
str(
349+
InvalidAttributeTypeError(
350+
attribute_name="source", expected_type=str
351+
)
352+
),
353+
]
354+
},
355+
),
356+
(
357+
{"id": "1", "source": "/", "type": ""},
358+
{
359+
"type": [
360+
str(
361+
InvalidAttributeValueError(
362+
attribute_name="type",
363+
msg="Attribute 'type' must not be None or empty",
309364
)
310365
)
311366
]
312367
},
313368
),
369+
(
370+
{"id": "1", "source": "/", "type": None},
371+
{
372+
"type": [
373+
str(
374+
InvalidAttributeValueError(
375+
attribute_name="type",
376+
msg="Attribute 'type' must not be None or empty",
377+
)
378+
),
379+
str(
380+
InvalidAttributeTypeError(
381+
attribute_name="type", expected_type=str
382+
)
383+
),
384+
]
385+
},
386+
),
387+
],
388+
)
389+
def test_required_attributes_null_or_empty(
390+
attributes: dict[str, Any], expected_errors: dict
391+
) -> None:
392+
with pytest.raises(CloudEventValidationError) as e:
393+
CloudEvent(attributes=attributes)
394+
395+
actual_errors = {
396+
key: [str(e) for e in value] for key, value in e.value.errors.items()
397+
}
398+
for key, expected_msgs in expected_errors.items():
399+
assert key in actual_errors
400+
assert actual_errors[key] == expected_msgs
401+
402+
403+
@pytest.mark.parametrize(
404+
"extension_name,expected_error",
405+
[
406+
(
407+
"",
408+
{
409+
"": [
410+
str(
411+
CustomExtensionAttributeError(
412+
"",
413+
"Extension attribute name must be at least 1 character long but was ''",
414+
)
415+
),
416+
str(
417+
CustomExtensionAttributeError(
418+
"",
419+
"Extension attribute '' should only contain lowercase letters and numbers",
420+
)
421+
),
422+
]
423+
},
424+
),
314425
(
315426
"data",
316427
{
@@ -344,6 +455,21 @@ def test_custom_extension(extension_name: str, expected_error: dict) -> None:
344455
assert actual_errors == expected_error
345456

346457

458+
def test_long_extension_attribute_name() -> None:
459+
# Verify that extension attribute names longer than 20 characters are allowed
460+
long_name = "a" * 21
461+
event = CloudEvent(
462+
{
463+
"id": "1",
464+
"source": "/",
465+
"type": "test",
466+
"specversion": "0.3",
467+
long_name: "value",
468+
}
469+
)
470+
assert event.get_extension(long_name) == "value"
471+
472+
347473
def test_default_specversion() -> None:
348474
event = CloudEvent(
349475
attributes={"source": "/source", "type": "test", "id": "1"},

0 commit comments

Comments
 (0)