diff --git a/TP/NGSI-LD/ContextInformation/Subscription/SubscriptionNotificationBehaviour/046_22_12.robot b/TP/NGSI-LD/ContextInformation/Subscription/SubscriptionNotificationBehaviour/046_22_12.robot new file mode 100644 index 0000000000000000000000000000000000000000..53d48aebba5b2b848ca39593ea1b103f64bc946e --- /dev/null +++ b/TP/NGSI-LD/ContextInformation/Subscription/SubscriptionNotificationBehaviour/046_22_12.robot @@ -0,0 +1,47 @@ +*** Settings *** +Documentation Check that a notification is sent in normalized format when an entity is deleted +... and attributeDeleted notification trigger is configured for a matching attribute in deleted entity + +Resource ${EXECDIR}/resources/ApiUtils/ContextInformationSubscription.resource +Resource ${EXECDIR}/resources/ApiUtils/ContextInformationProvision.resource +Resource ${EXECDIR}/resources/AssertionUtils.resource +Resource ${EXECDIR}/resources/NotificationUtils.resource +Resource ${EXECDIR}/resources/SubscriptionUtils.resource + +Test Setup Create Initial Subscription And Entity +Test Teardown Delete Initial Subscription And Entity + + +*** Variables *** +${subscription_payload_file_path}= subscriptions/subscription-building-entities-attributeDeleted-specific-attribute-normalized.jsonld +${building_filename}= building-simple-attributes.jsonld +${entity_expectation_file_path}= entity-deleted-name-attribute-on-entity-deleted-normalized.json + + +*** Test Cases *** +046_22_12 Check that a notification is sent with matching entity + [Documentation] Delete an entity with a matching attribute and check the received notification + [Tags] sub-notification 5_8_6 since_v1.6.1 + + ${response}= Delete Entity by Id ${entity_id} + Check Response Status Code 204 ${response.status_code} + + ${notification} ${headers}= Wait for notification timeout=${10} + + Should be Equal ${subscription_id} ${notification}[subscriptionId] + Length Should Be ${notification}[data] ${1} + Should be Equal ${entity_id} ${notification}[data][0][id] + Check Notification Containing Entity Element + ... ${entity_expectation_file_path} + ... ${notification} + + +*** Keywords *** +Create Initial Subscription And Entity + Create Subscription And Entity ${subscription_payload_file_path} ${building_filename} + Start Local Server ${notification_server_host} ${notification_server_port} + +Delete Initial Subscription And Entity + Delete Subscription ${subscription_id} + Delete Entity by Id ${entity_id} + Stop Local Server diff --git a/TP/NGSI-LD/ContextInformation/Subscription/SubscriptionNotificationBehaviour/046_22_13.robot b/TP/NGSI-LD/ContextInformation/Subscription/SubscriptionNotificationBehaviour/046_22_13.robot new file mode 100644 index 0000000000000000000000000000000000000000..dbe391b3c2ade77ab96829fdddfc074796f1217a --- /dev/null +++ b/TP/NGSI-LD/ContextInformation/Subscription/SubscriptionNotificationBehaviour/046_22_13.robot @@ -0,0 +1,47 @@ +*** Settings *** +Documentation Check that a notification is sent in normalized format with sysAttrs when an entity is deleted +... and attributeDeleted notification trigger is configured for a matching attribute in deleted entity + +Resource ${EXECDIR}/resources/ApiUtils/ContextInformationSubscription.resource +Resource ${EXECDIR}/resources/ApiUtils/ContextInformationProvision.resource +Resource ${EXECDIR}/resources/AssertionUtils.resource +Resource ${EXECDIR}/resources/NotificationUtils.resource +Resource ${EXECDIR}/resources/SubscriptionUtils.resource + +Test Setup Create Initial Subscription And Entity +Test Teardown Delete Initial Subscription And Entity + + +*** Variables *** +${subscription_payload_file_path}= subscriptions/subscription-building-entities-attributeDeleted-specific-attribute-normalized-sysAttrs.jsonld +${building_filename}= building-simple-attributes.jsonld +${entity_expectation_file_path}= entity-deleted-name-attribute-on-entity-deleted-normalized-sysAttrs.json + + +*** Test Cases *** +046_22_13 Check that a notification is sent with matching entity + [Documentation] Delete an entity with a matching attribute and check the received notification + [Tags] sub-notification 5_8_6 since_v1.6.1 + + ${response}= Delete Entity by Id ${entity_id} + Check Response Status Code 204 ${response.status_code} + + ${notification} ${headers}= Wait for notification timeout=${10} + + Should be Equal ${subscription_id} ${notification}[subscriptionId] + Length Should Be ${notification}[data] ${1} + Should be Equal ${entity_id} ${notification}[data][0][id] + Check Notification Containing Entity Element + ... ${entity_expectation_file_path} + ... ${notification} + + +*** Keywords *** +Create Initial Subscription And Entity + Create Subscription And Entity ${subscription_payload_file_path} ${building_filename} + Start Local Server ${notification_server_host} ${notification_server_port} + +Delete Initial Subscription And Entity + Delete Subscription ${subscription_id} + Delete Entity by Id ${entity_id} + Stop Local Server diff --git a/data/subscriptions/expectations/entity-deleted-name-attribute-on-entity-deleted-normalized-sysAttrs.json b/data/subscriptions/expectations/entity-deleted-name-attribute-on-entity-deleted-normalized-sysAttrs.json new file mode 100644 index 0000000000000000000000000000000000000000..7f5ffbbb60219b2c37e944d162be4f1dda7f1646 --- /dev/null +++ b/data/subscriptions/expectations/entity-deleted-name-attribute-on-entity-deleted-normalized-sysAttrs.json @@ -0,0 +1,14 @@ +{ + "id": "urn:ngsi-ld:Building:randomUUID", + "type": "Building", + "createdAt": "2025-04-13T14:30:57.950376Z", + "modifiedAt": "2025-04-13T14:30:57.950376Z", + "deletedAt": "2025-04-13T14:30:58.095375Z", + "name": { + "type": "Property", + "value": "urn:ngsi-ld:null", + "createdAt": "2025-04-13T14:30:57.950376Z", + "modifiedAt": "2025-04-13T14:30:57.950376Z", + "deletedAt": "2025-04-13T14:30:58.095375Z" + } +} \ No newline at end of file diff --git a/data/subscriptions/expectations/entity-deleted-name-attribute-on-entity-deleted-normalized.json b/data/subscriptions/expectations/entity-deleted-name-attribute-on-entity-deleted-normalized.json new file mode 100644 index 0000000000000000000000000000000000000000..e635e6e3da0f9e134d0c22a821bec5d1a5d00942 --- /dev/null +++ b/data/subscriptions/expectations/entity-deleted-name-attribute-on-entity-deleted-normalized.json @@ -0,0 +1,9 @@ +{ + "id": "urn:ngsi-ld:Building:randomUUID", + "type": "Building", + "deletedAt": "2025-04-13T14:06:03.199101Z", + "name": { + "type": "Property", + "value": "urn:ngsi-ld:null" + } +} \ No newline at end of file diff --git a/doc/files/ContextInformation/Subscription/046_22_12.json b/doc/files/ContextInformation/Subscription/046_22_12.json new file mode 100644 index 0000000000000000000000000000000000000000..1b38042281b80421eac3f9900e6f3e20470c0545 --- /dev/null +++ b/doc/files/ContextInformation/Subscription/046_22_12.json @@ -0,0 +1,39 @@ +{ + "tp_id": "TP/NGSI-LD/CI/SUB/046_22_12", + "test_objective": "Check that a notification is sent in normalized format when an entity is deleted\nand attributeDeleted notification trigger is configured for a matching attribute in deleted entity", + "reference": "ETSI GS CIM 009 V1.6.1 [], clause 5.8.6", + "config_id": "", + "parent_release": "v1.6.1", + "clauses": [ + "5.8.6" + ], + "pics_selection": "", + "keywords": [ + "Create Initial Subscription And Entity", + "Delete Initial Subscription And Entity" + ], + "teardown": "None", + "initial_condition": "with {\n the SUT being in the \"initial state\" and\n the SUT containing an initial Subscription ${subscription} \n with an id set to ${subscription_id}\n and an initial Entity ${entity}\n with an id set to ${entity_id}\n}", + "test_cases": [ + { + "name": "046_22_12 Check that a notification is sent with matching entity", + "permutation_tp_id": "TP/NGSI-LD/CI/SUB/046_22_12", + "doc": "Delete an entity with a matching attribute and check the received notification", + "tags": [ + "5_8_6", + "since_v1.6.1", + "sub-notification" + ], + "setup": "Create Initial Subscription And Entity", + "teardown": "Delete Initial Subscription And Entity", + "template": null, + "then": "then {\n the SUT sends a valid Response for the operations:\n Delete Entity by Id with Response Status Code set to 204 and\n Notification with Notification containing entity element set to 'entity-deleted-name-attribute-on-entity-deleted-normalized.json'\n}", + "when": "when {\n the SUT receives a Request from the client containing:\n URL set to '/ngsi-ld/v1/entities/{id}'\n method set to 'DELETE'\n Delete Entity Request with id set to ' ${entity_id}'\n}", + "http_verb": "DELETE", + "endpoint": "entities/{id}" + } + ], + "permutations": [], + "robotpath": "ContextInformation/Subscription/SubscriptionNotificationBehaviour", + "robotfile": "046_22_12" +} \ No newline at end of file diff --git a/doc/files/ContextInformation/Subscription/046_22_13.json b/doc/files/ContextInformation/Subscription/046_22_13.json new file mode 100644 index 0000000000000000000000000000000000000000..1b2f3d6e42777c8a92e2cd767f3bdb494a36de30 --- /dev/null +++ b/doc/files/ContextInformation/Subscription/046_22_13.json @@ -0,0 +1,39 @@ +{ + "tp_id": "TP/NGSI-LD/CI/SUB/046_22_13", + "test_objective": "Check that a notification is sent in normalized format with sysAttrs when an entity is deleted\nand attributeDeleted notification trigger is configured for a matching attribute in deleted entity", + "reference": "ETSI GS CIM 009 V1.6.1 [], clause 5.8.6", + "config_id": "", + "parent_release": "v1.6.1", + "clauses": [ + "5.8.6" + ], + "pics_selection": "", + "keywords": [ + "Create Initial Subscription And Entity", + "Delete Initial Subscription And Entity" + ], + "teardown": "None", + "initial_condition": "with {\n the SUT being in the \"initial state\" and\n the SUT containing an initial Subscription ${subscription} \n with an id set to ${subscription_id}\n and an initial Entity ${entity}\n with an id set to ${entity_id}\n}", + "test_cases": [ + { + "name": "046_22_13 Check that a notification is sent with matching entity", + "permutation_tp_id": "TP/NGSI-LD/CI/SUB/046_22_13", + "doc": "Delete an entity with a matching attribute and check the received notification", + "tags": [ + "5_8_6", + "since_v1.6.1", + "sub-notification" + ], + "setup": "Create Initial Subscription And Entity", + "teardown": "Delete Initial Subscription And Entity", + "template": null, + "then": "then {\n the SUT sends a valid Response for the operations:\n Delete Entity by Id with Response Status Code set to 204 and\n Notification with Notification containing entity element set to 'entity-deleted-name-attribute-on-entity-deleted-normalized-sysAttrs.json'\n}", + "when": "when {\n the SUT receives a Request from the client containing:\n URL set to '/ngsi-ld/v1/entities/{id}'\n method set to 'DELETE'\n Delete Entity Request with id set to ' ${entity_id}'\n}", + "http_verb": "DELETE", + "endpoint": "entities/{id}" + } + ], + "permutations": [], + "robotpath": "ContextInformation/Subscription/SubscriptionNotificationBehaviour", + "robotfile": "046_22_13" +} \ No newline at end of file diff --git a/doc/tests/test_ContextInformation_Subscription.py b/doc/tests/test_ContextInformation_Subscription.py index b2496348c33beb631d54fa8bc17a2e11e4abff60..4aa1bb2013016fd478c99bddaa879890a1cb8717 100644 --- a/doc/tests/test_ContextInformation_Subscription.py +++ b/doc/tests/test_ContextInformation_Subscription.py @@ -320,6 +320,20 @@ class TestCISubscription(TestCase): self.common_function(robot_file=robot_file, expected_value=expected_value, difference_file=difference_file) + def test_046_22_12(self): + robot_file = f'{self.folder_test_suites}/TP/NGSI-LD/ContextInformation/Subscription/SubscriptionNotificationBehaviour/046_22_12.robot' + expected_value = f'{self.folder_test_suites}/doc/files/ContextInformation/Subscription/046_22_12.json' + difference_file = f'{self.folder_test_suites}/doc/results/out_046_22_12.json' + + self.common_function(robot_file=robot_file, expected_value=expected_value, difference_file=difference_file) + + def test_046_22_13(self): + robot_file = f'{self.folder_test_suites}/TP/NGSI-LD/ContextInformation/Subscription/SubscriptionNotificationBehaviour/046_22_13.robot' + expected_value = f'{self.folder_test_suites}/doc/files/ContextInformation/Subscription/046_22_13.json' + difference_file = f'{self.folder_test_suites}/doc/results/out_046_22_13.json' + + self.common_function(robot_file=robot_file, expected_value=expected_value, difference_file=difference_file) + def test_031_01(self): robot_file = f'{self.folder_test_suites}/TP/NGSI-LD/ContextInformation/Subscription/QuerySubscriptions/031_01.robot' expected_value = f'{self.folder_test_suites}/doc/files/ContextInformation/Subscription/031_01.json' diff --git a/libraries/assertionUtils.py b/libraries/assertionUtils.py index edb706103142318e38b1f61e03d4241d563694b5..75def2bcc6097ca0734ac35c06208def0718b3f0 100644 --- a/libraries/assertionUtils.py +++ b/libraries/assertionUtils.py @@ -45,17 +45,27 @@ class StringOrSingleListContextOperator: return expected_context == actual_context -class TemporalPropertyOperator: +# for observedAt, check there is a strict equality between expected and actual +class ObservedAtPropertyOperator: + def match(self, level) -> bool: + return level.path().endswith("['observedAt']") + + def give_up_diffing(self, level, diff_instance) -> bool: + expected_datetime = dateTimeUtils.parse_ngsild_date(level.t1) + actual_datetime = dateTimeUtils.parse_ngsild_date(level.t2) + return actual_datetime is not None and expected_datetime == actual_datetime + + +# for system generated temporal properties, only check it is present and has the correct format +class SystemGeneratedTemporalPropertyOperator: def match(self, level) -> bool: return (level.path().endswith("['createdAt']") or level.path().endswith("['modifiedAt']") - or level.path().endswith("['observedAt']") or level.path().endswith("['deletedAt']")) def give_up_diffing(self, level, diff_instance) -> bool: - expected_datetime = dateTimeUtils.parse_ngsild_date(level.t1) actual_datetime = dateTimeUtils.parse_ngsild_date(level.t2) - return actual_datetime is not None and expected_datetime == actual_datetime + return actual_datetime is not None def compare_func(x, y, level=None): @@ -78,21 +88,37 @@ def compare_dictionaries_ignoring_keys(expected, actual, exclude_regex_paths, ig 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, iterable_compare_func=compare_func, - custom_operators=[AnyCoreContextVersionOperator(), TemporalPropertyOperator()], + custom_operators=[ + AnyCoreContextVersionOperator(), + ObservedAtPropertyOperator(), + SystemGeneratedTemporalPropertyOperator() + ], 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, iterable_compare_func=compare_func, - custom_operators=[StringOrSingleListContextOperator(), TemporalPropertyOperator()], + custom_operators=[ + StringOrSingleListContextOperator(), + ObservedAtPropertyOperator(), + SystemGeneratedTemporalPropertyOperator() + ], 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, iterable_compare_func=compare_func, - custom_operators=[AnyCoreContextVersionOperator(), TemporalPropertyOperator()]) + custom_operators=[ + AnyCoreContextVersionOperator(), + ObservedAtPropertyOperator(), + SystemGeneratedTemporalPropertyOperator() + ]) else: res = DeepDiff(expected, actual, exclude_regex_paths=exclude_regex_paths, ignore_order=True, verbose_level=1, iterable_compare_func=compare_func, - custom_operators=[StringOrSingleListContextOperator(), TemporalPropertyOperator()]) + custom_operators=[ + StringOrSingleListContextOperator(), + ObservedAtPropertyOperator(), + SystemGeneratedTemporalPropertyOperator() + ]) if len(res) > 0: output_pretty_diff(expected, actual, Theme(added="", removed="", reset=""))