diff --git a/TP/NGSI-LD/ContextSource/RegistrationSubscription/CreateContextSourceRegistrationSubscription/038_02.robot b/TP/NGSI-LD/ContextSource/RegistrationSubscription/CreateContextSourceRegistrationSubscription/038_02.robot index e50bf18fffcb77dc91951207971b68c1feb9a35d..3875fb9fbc4cf9cacc1081bdfbdc052dd3b2f237 100644 --- a/TP/NGSI-LD/ContextSource/RegistrationSubscription/CreateContextSourceRegistrationSubscription/038_02.robot +++ b/TP/NGSI-LD/ContextSource/RegistrationSubscription/CreateContextSourceRegistrationSubscription/038_02.robot @@ -22,7 +22,10 @@ ${subscription_id}= ${EMPTY} ${response}= Create Context Source Registration Subscription ${subscription_payload} Dictionary Should Contain Key ${response.headers} Location msg=HTTP Headers do not contain key 'Location' - ${subscription_id}= Get From Dictionary ${response.headers} Location + ${location}= Get From Dictionary ${response.headers} Location + # Location is a URI reference (RFC 9110 § 10.2.2) — typically the resource + # path /ngsi-ld/v1/csourceSubscriptions/. Extract the generated id. + ${subscription_id}= Fetch From Right ${location} / Set Suite Variable ${subscription_id} Check Response Status Code 201 ${response.status_code} @@ -32,7 +35,18 @@ ${subscription_id}= ${EMPTY} ... subscription_id=${subscription_id} ... context=${ngsild_test_suite_context} ... accept=${CONTENT_TYPE_LD_JSON} - ${ignored_attributes}= Create List ${id_regex_expr} ${status_regex_expr} + # Ignore the Additional Members ('lastFailure', 'lastNotification', 'timesFailed', + # 'timesSent', 'status'), both at root level and inside 'notification' (clause + # 5.2.14), where they may have been set already by the initial notification sent + # upon subscription creation (clause 12.4.7). + ${ignored_attributes}= Create List + ... ${id_regex_expr} + ... ${status_regex_expr} + ... ${lastfailure_regex_expr} + ... ${lastNotification_regex_expr} + ... ${timesFailed_regex_expr} + ... ${timesSent_regex_expr} + ... ${notification_result_regex_expr} Check Created Resource Set To ${subscription_payload} ${response1.json()} ${ignored_attributes} diff --git a/data/csourceSubscriptions/subscription-invalid-query.jsonld b/data/csourceSubscriptions/subscription-invalid-query.jsonld index ff185d6ca2325e7cb609d745c50f8a62abfec9c3..ed33311f1bda997957c4f044d59cb9fd8c8d4221 100644 --- a/data/csourceSubscriptions/subscription-invalid-query.jsonld +++ b/data/csourceSubscriptions/subscription-invalid-query.jsonld @@ -6,7 +6,7 @@ "type":"Building" } ], - "q": "invalidQuery", + "q": "(invalidQuery", "notification":{ "format":"keyValues", "endpoint":{ diff --git a/libraries/assertionUtils.py b/libraries/assertionUtils.py index 14b35fbaad7f85070dbb7a10837dbfe478faa5bc..3e186153381ed8621b204b941275c3e73ff3cb8b 100644 --- a/libraries/assertionUtils.py +++ b/libraries/assertionUtils.py @@ -98,10 +98,16 @@ def compare_dictionaries_ignoring_keys(expected, actual, exclude_regex_paths, ig :param exclude_regex_paths: list of regex paths of keys to be ignored :param ignore_core_context_version: whether any core context version is allowed in the results :param group_by: a key to group the results, useful for lists of results + + threshold_to_diff_deeper=0: since deepdiff 8, when more than 1/3 of a dict's keys + differ, DeepDiff reports the whole dict as changed instead of descending into it, + which silently bypasses exclude_regex_paths targeting the nested keys (e.g. the + broker-computed Additional Members inside 'notification'). 0 restores per-key diffs. """ if group_by is not None and ignore_core_context_version: res = DeepDiff(expected, actual, exclude_regex_paths=exclude_regex_paths, ignore_order=True, verbose_level=1, + threshold_to_diff_deeper=0, iterable_compare_func=compare_func, custom_operators=[ AnyCoreContextVersionOperator(), @@ -111,6 +117,7 @@ def compare_dictionaries_ignoring_keys(expected, actual, exclude_regex_paths, ig group_by=group_by) elif group_by is not None: res = DeepDiff(expected, actual, exclude_regex_paths=exclude_regex_paths, ignore_order=True, verbose_level=1, + threshold_to_diff_deeper=0, iterable_compare_func=compare_func, custom_operators=[ StringOrSingleListContextOperator(), @@ -120,6 +127,7 @@ def compare_dictionaries_ignoring_keys(expected, actual, exclude_regex_paths, ig group_by=group_by) elif ignore_core_context_version: res = DeepDiff(expected, actual, exclude_regex_paths=exclude_regex_paths, ignore_order=True, verbose_level=1, + threshold_to_diff_deeper=0, iterable_compare_func=compare_func, custom_operators=[ AnyCoreContextVersionOperator(), @@ -128,6 +136,7 @@ def compare_dictionaries_ignoring_keys(expected, actual, exclude_regex_paths, ig ]) else: res = DeepDiff(expected, actual, exclude_regex_paths=exclude_regex_paths, ignore_order=True, verbose_level=1, + threshold_to_diff_deeper=0, iterable_compare_func=compare_func, custom_operators=[ StringOrSingleListContextOperator(), diff --git a/resources/AssertionUtils.resource b/resources/AssertionUtils.resource index 6b75603dcf211e87b65f48e286c4a06b1f23b5b5..c21eaa5c966af50c30ceda287edd5396b498d180 100755 --- a/resources/AssertionUtils.resource +++ b/resources/AssertionUtils.resource @@ -20,6 +20,7 @@ ${lastNotification_regex_expr}= root\\['lastNotification'\\] ${timesFailed_regex_expr}= root\\['timesFailed'\\] ${timesSent_regex_expr}= root\\['timesSent'\\] ${is_active_expr}= root\\['isActive'\\] +${notification_result_regex_expr}= \\['notification'\\]\\['(lastNotification|lastSuccess|lastFailure|timesSent|timesFailed|status)'\\] ${context_source_ignored_regex_expr}= root\\['@context'\\] diff --git a/testsuite-doubts.md b/testsuite-doubts.md index 50417d0d9ed842c484c57b911fbd007b17b2d9cc..fe3e942919532bcaa3e10463e200475b512d6e07 100644 --- a/testsuite-doubts.md +++ b/testsuite-doubts.md @@ -2667,3 +2667,32 @@ doesn't match the URL id. `$.id` with the actual `${entity_id}` before PATCHing. Or omit the `id` from the fragment entirely (PATCH attrs doesn't need it). + + +## 96. `038_02_01` / `038_08_03` — Location parsed as an id, and a syntactically valid `q` expected to fail + +Two independent defects in the csourceSubscription-creation tests; both also +LEAK their subscription (the teardown's delete fails / never runs), and the +leaked subscriptions then poison the `041_*` count assertions +(`Query All Subscriptions: 5 != 3` etc.) in full-suite runs. + +**a) `038_02_01` (create without id):** the test stores the raw `Location` +response header as `${subscription_id}`. `Location` is a URI reference +(RFC 9110 § 10.2.2) — brokers return the resource path +`/ngsi-ld/v1/csourceSubscriptions/` — so the follow-up retrieve and the +teardown delete build doubled URLs and fail. Fixed by extracting the last +path segment. The retrieve comparison additionally needs the clause-12.4.7 +Additional-Members ignores (root level and inside `notification`) like its +siblings 038_01/038_03 — the initial notification to the dead fixture +endpoint sets `timesSent`/`timesFailed`/`lastNotification`/`lastFailure`/ +`status` immediately. + +**b) `038_08_03` (InvalidQuery):** the fixture's `"q": "invalidQuery"` is a +bare attribute path — per TS 104-175 § 7.x ABNF (`QueryTerm = Attribute`) +that is a syntactically VALID query (an existence predicate), so a broker +must accept it with 201. The fixture now uses `"(invalidQuery"` (unbalanced +parenthesis — genuinely invalid), mirroring `037_03_02`. + +(The `notification_result_regex_expr` variable and the assertionUtils.py +`threshold_to_diff_deeper=0` fix are re-declared identically to their origin +branch since that MR is unmerged — git deduplicates on merge.)