diff --git a/monitoring/monitorlib/fetch/__init__.py b/monitoring/monitorlib/fetch/__init__.py index 74a0f44841..99b30c0a4c 100644 --- a/monitoring/monitorlib/fetch/__init__.py +++ b/monitoring/monitorlib/fetch/__init__.py @@ -459,6 +459,19 @@ def __init__(self, msg: str, queries: Optional[Union[Query, List[Query]]] = None else: self.queries = queries + @property + def last_status_code(self) -> int: + """Returns the status code of the last query present in this QueryError, + or 999 if this error contains no queries.""" + if len(self.queries) == 0: + return 999 + return self.queries[-1].status_code + + @property + def query_timestamps(self) -> List[datetime.datetime]: + """Returns the timestamps of all queries present in this QueryError.""" + return [q.request.timestamp for q in self.queries] + @property def stacktrace(self) -> str: return stacktrace_string(self) diff --git a/monitoring/monitorlib/schema_validation.py b/monitoring/monitorlib/schema_validation.py index 8865e09914..6bcb9363ff 100644 --- a/monitoring/monitorlib/schema_validation.py +++ b/monitoring/monitorlib/schema_validation.py @@ -58,6 +58,16 @@ class F3548_21(str, Enum): QuerySubscriptionsResponse = "components.schemas.QuerySubscriptionsResponse" DeleteSubscriptionResponse = "components.schemas.DeleteSubscriptionResponse" + ChangeOperationalIntentReferenceResponse = ( + "components.schemas.ChangeOperationalIntentReferenceResponse" + ) + GetOperationalIntentReferenceResponse = ( + "components.schemas.GetOperationalIntentReferenceResponse" + ) + QueryOperationalIntentReferenceResponse = ( + "components.schemas.QueryOperationalIntentReferenceResponse" + ) + _openapi_content_cache: Dict[str, dict] = {} diff --git a/monitoring/prober/infrastructure.py b/monitoring/prober/infrastructure.py index d543476a4f..1aadafa3b6 100644 --- a/monitoring/prober/infrastructure.py +++ b/monitoring/prober/infrastructure.py @@ -100,7 +100,7 @@ def wrapper_default_scope(*args, **kwargs): resource_type_code_descriptions: Dict[ResourceType, str] = {} -# Next code: 380 +# Next code: 382 def register_resource_type(code: int, description: str) -> ResourceType: """Register that the specified code refers to the described resource. diff --git a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml index c5d165c3fe..2236fe9415 100644 --- a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml +++ b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml @@ -23,6 +23,7 @@ v1: dss_instances: dss_instances mock_uss: mock_uss second_utm_auth: second_utm_auth + utm_client_identity: utm_client_identity planning_area: planning_area problematically_big_area: problematically_big_area diff --git a/monitoring/uss_qualifier/configurations/dev/message_signing.yaml b/monitoring/uss_qualifier/configurations/dev/message_signing.yaml index 56ccd5028b..a81f809f53 100644 --- a/monitoring/uss_qualifier/configurations/dev/message_signing.yaml +++ b/monitoring/uss_qualifier/configurations/dev/message_signing.yaml @@ -60,6 +60,7 @@ v1: dss_instances: scd_dss_instances id_generator: id_generator second_utm_auth: second_utm_auth + utm_client_identity: utm_client_identity planning_area: che_planning_area problematically_big_area: che_problematically_big_area execution: diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/planning_area.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/planning_area.py index 73bf102364..ce7e204a0d 100644 --- a/monitoring/uss_qualifier/resources/astm/f3548/v21/planning_area.py +++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/planning_area.py @@ -2,9 +2,18 @@ from typing import List, Dict, Any, Optional, Self from implicitdict import ImplicitDict, StringBasedDateTime -from uas_standards.astm.f3548.v21.api import Volume4D +from uas_standards.astm.f3548.v21.api import ( + EntityOVN, + OperationalIntentState, + UssBaseURL, + EntityID, + PutOperationalIntentReferenceParameters, + ImplicitSubscriptionParameters, +) from monitoring.monitorlib.geo import LatLngPoint, make_latlng_rect, Volume3D, Polygon +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.monitorlib.temporal import Time from monitoring.uss_qualifier.resources.astm.f3548.v21.subscription_params import ( SubscriptionParams, ) @@ -52,6 +61,46 @@ def get_new_subscription_params( notify_for_constraints=notify_for_constraints, ) + def get_new_operational_intent_ref_params( + self, + key: List[EntityOVN], + state: OperationalIntentState, + uss_base_url: UssBaseURL, + time_start: datetime.datetime, + time_end: datetime.datetime, + oir_id: Optional[EntityID], + implicit_sub_base_url: Optional[UssBaseURL] = None, + implicit_sub_for_constraints: Optional[bool] = None, + ) -> PutOperationalIntentReferenceParameters: + """ + Build a PutOperationalIntentReferenceParameters object that can be used against the DSS OIR API. + + The extents contained in these parameters contain a single 4DVolume, which may not be entirely realistic, + but is sufficient in situations where the content of the OIR is irrelevant as long as it is valid, such + as for testing authentication or parameter validation. + + Note that this method allows building inconsistent parameters. + """ + return PutOperationalIntentReferenceParameters( + extents=[ + Volume4D( + volume=self.volume, + time_start=Time(time_start), + time_end=Time(time_end), + ).to_f3548v21() + ], + key=key, + state=state, + uss_base_url=uss_base_url, + subscription_id=oir_id, + new_subscription=ImplicitSubscriptionParameters( + uss_base_url=implicit_sub_base_url, + notify_for_constraints=implicit_sub_for_constraints, + ) + if implicit_sub_base_url + else None, + ) + class PlanningAreaResource(Resource[PlanningAreaSpecification]): specification: PlanningAreaSpecification diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/clean_workspace.md b/monitoring/uss_qualifier/scenarios/astm/utm/dss/clean_workspace.md index bfa594d92b..cfc486249c 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/clean_workspace.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/clean_workspace.md @@ -4,8 +4,8 @@ This page describes the content of a common test step that ensures a clean works ## 🛑 Operational intent references can be queried by ID check -If an existing operational intent reference cannot directly be queried by its ID, the DSS implementation is in violation of -**[astm.f3548.v21.DSS0005,1](../../../../requirements/astm/f3548/v21.md)**. +If an existing operational intent reference cannot directly be queried by its ID, or if for a non-existing one the DSS replies with a status code different than 404, +the DSS implementation is in violation of **[astm.f3548.v21.DSS0005,1](../../../../requirements/astm/f3548/v21.md)**. ## 🛑 Operational intent references can be searched for check diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_crud.md b/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_crud.md new file mode 100644 index 0000000000..04f45b62b3 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_crud.md @@ -0,0 +1,78 @@ +# CRUD operational intent reference test step fragment + +This test step fragment validates that operational intent references can be created, updated, read and modified. + +## 🛑 Create operational intent reference query succeeds check + +As per **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**, the DSS API must allow callers to create an operational intent reference with either one or both of the +start and end time missing, provided all the required parameters are valid. + +## 🛑 Create operational intent reference response is correct check + +A successful operational intent reference creation query is expected to return a well-defined body, the content of which reflects the created operational intent reference. +If the format and content of the response are not conforming, the DSS is failing to implement **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Create operational intent reference response format conforms to spec check + +The response to a successful operational intent reference creation query is expected to conform to the format defined by the OpenAPI specification under the `A3.1` Annex of ASTM F3548−21. + +If it does not, the DSS is failing to implement **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Response to operational intent reference creation contains correct body check + +As per **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**, upon creation of a operational intent reference, +the newly created operational intent reference must be part of its response. + +## 🛑 Get operational intent reference by ID check + +If an operational intent reference cannot be queried using its ID, the DSS is failing to meet **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Get operational intent reference response format conforms to spec check + +The response to a successful get operational intent reference query is expected to conform to the format defined by the OpenAPI specification under the `A3.1` Annex of ASTM F3548−21. + +If it does not, the DSS is failing to implement **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Successful operational intent reference search query check + +If the DSS fails to let us search in the area for which the OIR was created, it is failing to meet **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Created operational intent reference is in search results check + +If the existing operational intent reference is not returned in a search that covers the area it was created for, the DSS is not properly implementing **[astm.f3548.v21.DSS0005,2](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Search operational intent reference response format conforms to spec check + +The response to a successful operational intent reference search query is expected to conform to the format defined by the OpenAPI specification under the `A3.1` Annex of ASTM F3548−21. + +If it does not, the DSS is failing to implement **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Mutate operational intent reference query succeeds check + +As per **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**, the DSS API must allow callers to mutate an operational intent reference. + +## 🛑 Mutate operational intent reference query succeeds check + +A successful operational intent reference mutation query is expected to return a well-defined body, the content of which reflects the updated operational intent reference. +If the format and content of the response are not conforming, the DSS is failing to implement **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Mutate operational intent reference response format conforms to spec check + +The response to a successful operational intent reference mutation query is expected to conform to the format defined by the OpenAPI specification under the `A3.1` Annex of ASTM F3548−21. + +If it does not, the DSS is failing to implement **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Delete operational intent reference query succeeds check + +A query to delete an operational intent reference, by its owner and when the correct OVN is provided, should succeed, otherwise the DSS is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Delete operational intent reference response is correct check + +A successful operational intent reference deletion query is expected to return a well-defined body, the content of which reflects the operational intent reference at the moment of deletion. +If the format and content of the response are not conforming, the DSS is failing to implement **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Delete operational intent reference response format conforms to spec check + +The response to a successful operational intent reference deletion query is expected to conform to the format defined by the OpenAPI specification under the `A3.1` Annex of ASTM F3548−21. + +If it does not, the DSS is failing to implement **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_sync.md b/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_sync.md new file mode 100644 index 0000000000..7d1705e8c0 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_sync.md @@ -0,0 +1,33 @@ +# Synchronize operational intent reference test step fragment + +This test step fragment validates that operational intent references are properly synchronized across a set of DSS instances. + +## 🛑 Operational intent reference can be found at every DSS check + +If the previously created or mutated operational intent reference cannot be found at a DSS, either one of the instances at which the operational intent reference was created or the one that was queried, +may be failing to implement **[astm.f3548.v21.DSS0210,2a](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Propagated operational intent reference contains the correct manager check + +If the operational intent reference returned by a DSS to which the operational intent reference was synchronized to does not contain the correct manager, +either one of the instances at which the operational intent reference was created or the one that was queried, may be failing to implement **[astm.f3548.v21.DSS0210,2b](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Propagated operational intent reference contains the correct USS base URL check + +If the operational intent reference returned by a DSS to which the operational intent reference was synchronized to does not contain the correct USS base URL, +either one of the instances at which the operational intent reference was created or the one that was queried, may be failing to implement **[astm.f3548.v21.DSS0210,2c](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Propagated operational intent reference contains the correct state check + +If the operational intent reference returned by a DSS to which the operational intent reference was synchronized to does not contain the correct state, +either one of the instances at which the operational intent reference was created or the one that was queried, may be failing to implement **[astm.f3548.v21.DSS0210,2d](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Propagated operational intent reference contains the correct start time check + +If the operational intent reference returned by a DSS to which the operational intent reference was synchronized to does not contain the correct start time, +either one of the instances at which the operational intent reference was created or the one that was queried, may be failing to implement **[astm.f3548.v21.DSS0210,2f](../../../../../requirements/astm/f3548/v21.md)**. + +## 🛑 Propagated operational intent reference contains the correct end time check + +If the operational intent reference returned by a DSS to which the operational intent reference was synchronized to does not contain the correct end time, +either one of the instances at which the operational intent reference was created or the one that was queried, may be failing to implement **[astm.f3548.v21.DSS0210,2f](../../../../../requirements/astm/f3548/v21.md)**. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_validate.md b/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_validate.md new file mode 100644 index 0000000000..9a316bec1a --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/fragments/oir_validate.md @@ -0,0 +1,67 @@ +# Validate operational intent reference test step fragment + +This test step fragment attempts to validate a single operational intent reference returned by the DSS. + +The code for these checks lives in the [oir_validator.py](../validators/oir_validator.py) class. + +## ⚠️ Returned operational intent reference ID is correct check + +If the returned operational intent reference ID does not correspond to the one specified in the creation parameters, +**[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)** is not respected. + +## ⚠️ Returned operational intent reference has a manager check + +If the returned operational intent reference has no manager defined, **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)** is not respected. + +## ⚠️ Returned operational intent reference manager is correct check + +The returned manager must correspond to the identity of the client that created the operational intent at the DSS, +otherwise the DSS is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Returned operational intent reference state is correct check + +The returned state must be the same as the provided one, otherwise the DSS is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Returned operational intent reference has an USS base URL check + +If the returned operational intent reference has no USS base URL defined, **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)** is not respected. + +## ⚠️ Returned operational intent reference base URL is correct check + +The returned USS base URL must be prefixed with the USS base URL that was provided at operational intent reference creation, otherwise the DSS is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Returned operational intent reference has a start time check + +If the returned operational intent reference has no start time defined, **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)** is not respected. + +## ⚠️ Returned start time is correct check + +The returned start time must be the same as the provided one, otherwise the DSS is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Returned operational intent reference has an end time check + +Operational intent references need a defined end time in order to limit their duration: if the DSS omits to set the end time, it will be in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Returned end time is correct check + +The returned end time must be the same as the provided one, otherwise the DSS is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Returned operational intent reference has a version check + +If the returned operational intent reference has no version defined, **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)** is not respected. + +## ⚠️ Non-mutated operational intent reference keeps the same version check + +If the version of the operational intent reference is updated without there having been any mutation of the operational intent reference, the DSS is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Non-mutated operational intent reference keeps the same OVN check + +If the OVN of the operational intent reference is updated without there having been any mutation of the operational intent reference, the DSS is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Mutated operational intent reference version is updated check + +Following a mutation, the DSS needs to update the operational intent reference version, otherwise it is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ Mutated operational intent reference OVN is updated check + +Following a mutation, the DSS needs to update the operational intent reference OVN, otherwise it is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/remove_op_intent.md b/monitoring/uss_qualifier/scenarios/astm/utm/dss/remove_op_intent.md deleted file mode 100644 index c154feaf50..0000000000 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/remove_op_intent.md +++ /dev/null @@ -1,7 +0,0 @@ -# Remove operational intent test step fragment - -This test step fragment attempts to remove from the DSS a specific operational intent reference managed by a user whose credentials are provided to uss_qualifier. - -## 🛑 Operational intent reference removed check - -If the operational intent reference could not be removed, the DSS instance used does not meet **[astm.f3548.v21.DSS0005,1](../../../../requirements/astm/f3548/v21.md)** diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/__init__.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/__init__.py index 3ac599b519..9aa039a538 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/__init__.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/__init__.py @@ -1 +1,2 @@ from .subscription_synchronization import SubscriptionSynchronization +from .op_intent_ref_synchronization import OIRSynchronization diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.md b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.md new file mode 100644 index 0000000000..886d1f67d1 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.md @@ -0,0 +1,132 @@ +# ASTM SCD DSS: Operational Intent Reference Synchronization test scenario + +## Overview + +Verifies that all CRUD operations on operational intent references performed on a given DSS instance +are properly propagated to every other DSS instances participating in the deployment. + +## Resources + +### dss + +[`DSSInstanceResource`](../../../../../resources/astm/f3548/v21/dss.py) the DSS instance through which entities are created, modified and deleted. + +### other_instances + +[`DSSInstancesResource`](../../../../../resources/astm/f3548/v21/dss.py) pointing to the DSS instances used to confirm that entities are properly propagated. + +### id_generator + +[`IDGeneratorResource`](../../../../../resources/interuss/id_generator.py) providing the Subscription ID for this scenario. + +### planning_area + +[`PlanningAreaResource`](../../../../../resources/astm/f3548/v21/planning_area.py) describes the 3D volume in which subscriptions will be created. + +### client_identity + +[`ClientIdentityResource`](../../../../../resources/communications/client_identity.py) to be used for this scenario. + +## Setup test case + +### [Ensure clean workspace test step](../clean_workspace.md) + +This step ensures that no subscription with the known test ID exists in the DSS. + +## OIR synchronization test case + +This test case creates an operational intent reference on the main DSS, and verifies that it is properly synchronized to the other DSS instances. + +It then goes on to mutate and delete it, each time confirming that all other DSSes return the expected results. + +### Create OIR validation test step + +#### [Create OIR](../fragments/oir_crud.md) + +Verify that an operational intent reference can be created on the primary DSS. + +#### [Validate OIR](../fragments/oir_validate.md) + +Verify that the operational intent reference returned by the DSS under test is properly formatted and contains the expected content. + +### Query newly created OIR test step + +Query the created operational intent at every DSS provided in `dss_instances`. + +#### 🛑 Operational intent reference returned by a secondary DSS is valid and correct check + +When queried for an operational intent reference that was created via another DSS, a DSS instance is expected to provide a valid operational intent reference. + +If it does not, it might be in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +#### [OIR is synchronized](../fragments/oir_sync.md) + +Confirm that the operational intent reference that was just created is properly synchronized across all DSS instances. + +#### [Get OIR](../fragments/oir_crud.md) + +Confirms that each DSS provides access to the created operational intent reference, + +#### [Validate OIR](../fragments/oir_validate.md) + +Verify that the operational intent reference returned by every DSS is correctly formatted and corresponds to what was created earlier. + +### Mutate OIR test step + +This test step mutates the previously created operational intent reference to verify that the DSS reacts properly: notably, it checks that the operational intent reference version is updated, +including for changes that are not directly visible, such as changing the operational intent reference's footprint. + +#### [Update OIR](../fragments/oir_crud.md) + +Confirm that the operational intent reference can be mutated. + +#### [Validate OIR](../fragments/oir_validate.md) + +Verify that the operational intent reference returned by the DSS is properly formatted and contains the correct content. + +### Query updated OIR test step + +Query the updated operational intent reference at every DSS provided in `dss_instances`. + +#### 🛑 Operational intent reference returned by a secondary DSS is valid and correct check + +When queried for an operational intent reference that was mutated via another DSS, a DSS instance is expected to provide a valid operational intent reference. + +If it does not, it might be in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +#### [OIR is synchronized](../fragments/oir_sync.md) + +Confirm that the operational intent reference that was just mutated is properly synchronized across all DSS instances. + +#### [Get OIR](../fragments/oir_crud.md) + +Confirms that the operational intent reference that was just mutated can be retrieved from any DSS. + +#### [Validate OIR](../fragments/oir_validate.md) + +Verify that the operational intent reference returned by every DSS is correctly formatted and corresponds to what was mutated earlier. + +### Delete OIR test step + +Attempt to delete the operational intent reference in various ways and ensure that the DSS reacts properly. + +This also checks that the operational intent reference data returned by a successful deletion is correct. + +#### [Delete OIR](../fragments/oir_crud.md) + +Confirms that an operational intent reference can be deleted. + +#### [Validate OIR](../fragments/oir_validate.md) + +Verify that the operational intent reference returned by the DSS via the deletion is properly formatted and contains the correct content. + +### Query deleted OIR test step + +Attempt to query and search for the deleted operational intent reference in various ways + +#### 🛑 Secondary DSS should not return the deleted operational intent reference check + +If a DSS returns an operational intent reference that was previously successfully deleted from the primary DSS, +either one of the primary DSS or the DSS that returned the operational intent reference is in violation of **[astm.f3548.v21.DSS0210,2a](../../../../../requirements/astm/f3548/v21.md)**. + +## [Cleanup](../clean_workspace.md) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py new file mode 100644 index 0000000000..9270d01825 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py @@ -0,0 +1,484 @@ +from datetime import datetime, timedelta +from typing import List, Optional + +import loguru +from implicitdict import StringBasedDateTime +from uas_standards.astm.f3548.v21 import api +from uas_standards.astm.f3548.v21.api import ( + OperationalIntentReference, + PutOperationalIntentReferenceParameters, + EntityID, + OperationalIntentState, +) +from uas_standards.astm.f3548.v21.constants import Scope + +from monitoring.monitorlib.fetch import QueryError +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.prober.infrastructure import register_resource_type +from monitoring.uss_qualifier.resources.astm.f3548.v21 import PlanningAreaResource +from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import ( + DSSInstanceResource, + DSSInstancesResource, + DSSInstance, +) +from monitoring.uss_qualifier.resources.communications import ClientIdentityResource +from monitoring.uss_qualifier.resources.interuss.id_generator import IDGeneratorResource +from monitoring.uss_qualifier.scenarios.astm.utm.dss import test_step_fragments +from monitoring.uss_qualifier.scenarios.astm.utm.dss.validators.oir_validator import ( + OIRValidator, + TIME_TOLERANCE_SEC, +) +from monitoring.uss_qualifier.scenarios.scenario import ( + TestScenario, +) +from monitoring.uss_qualifier.suites.suite import ExecutionContext + + +class OIRSynchronization(TestScenario): + """ + A scenario that checks if multiple DSS instances properly synchronize + operational intent references. + + Not in the scope of the first version of this: + - access rights (making sure only the manager of the OIR can mutate it) + - control of the area synchronization (by doing area searches against the secondaries) + - mutation of an entity on a secondary DSS when it was created on the primary + - deletion of an entity on a secondary DSS when it was created on the primary + """ + + SUB_TYPE = register_resource_type(381, "Operational Intent Reference") + + _dss: DSSInstance + + _dss_read_instances: List[DSSInstance] + + # Base identifier for the OIR that will be created + _oir_id: EntityID + + # Base parameters used for OIR creation + _oir_params: PutOperationalIntentReferenceParameters + + # Keep track of the current OIR state + _current_oir: Optional[OperationalIntentReference] + + _expected_manager: str + + def __init__( + self, + dss: DSSInstanceResource, + other_instances: DSSInstancesResource, + id_generator: IDGeneratorResource, + client_identity: ClientIdentityResource, + planning_area: PlanningAreaResource, + ): + """ + Args: + dss: dss to test + other_instances: dss instances to be checked for proper synchronization + id_generator: will let us generate specific identifiers + client_identity: tells us the identity we should expect as an entity's manager + planning_area: An Area to use for the tests. It should be an area for which the DSS is responsible, + but has no other requirements. + + """ + super().__init__() + scopes_primary = { + Scope.StrategicCoordination: "create and delete operational intent references" + } + scopes_read = {Scope.StrategicCoordination: "read operational intents"} + + self._dss = dss.get_instance(scopes_primary) + self._primary_pid = self._dss.participant_id + + self._dss_read_instances = [ + sec_dss.get_instance(scopes_read) + for sec_dss in other_instances.dss_instances + ] + + self._oir_id = id_generator.id_factory.make_id(self.SUB_TYPE) + self._expected_manager = client_identity.subscriber() + self._planning_area = planning_area.specification + + # Build a ready-to-use 4D volume with no specified time for searching + # the currently active OIRs + # TODO OIR search will be added in an upcomming PR + self._planning_area_volume4d = Volume4D( + volume=self._planning_area.volume, + ) + + self._oir_params = self._planning_area.get_new_operational_intent_ref_params( + key=[], + state=OperationalIntentState.Accepted, + uss_base_url=self._planning_area.base_url, + time_start=datetime.now() - timedelta(seconds=10), + time_end=datetime.now() + timedelta(minutes=20), + oir_id=None, + implicit_sub_base_url=None, + implicit_sub_for_constraints=None, + ) + + def run(self, context: ExecutionContext): + + # Check that we actually have at least one other DSS to test against: + if not self._dss_read_instances: + loguru.logger.warning( + "Skipping EntitySynchronization test: no other DSS instances to test against" + ) + return + + self.begin_test_scenario(context) + self._setup_case() + self.begin_test_case("OIR synchronization") + + self.begin_test_step("Create OIR validation") + self._create_oir_with_params(self._oir_params) + self.end_test_step() + + self.begin_test_step("Query newly created OIR") + self._query_secondaries_and_compare(self._oir_params) + self.end_test_step() + + self.begin_test_step("Mutate OIR") + self._test_mutate_oir_shift_time() + self.end_test_step() + + self.begin_test_step("Query updated OIR") + self._query_secondaries_and_compare(self._oir_params) + self.end_test_step() + + self.begin_test_step("Delete OIR") + self._test_delete_sub() + self.end_test_step() + + self.begin_test_step("Query deleted OIR") + self._test_get_deleted_oir() + self.end_test_step() + + self.end_test_case() + self.end_test_scenario() + + def _setup_case(self): + self.begin_test_case("Setup") + # Multiple runs of the scenario seem to rely on the same instance of it: + # thus we need to reset the state of the scenario before running it. + self._current_oir = None + self.begin_test_step("Ensure clean workspace") + self._ensure_clean_workspace_step() + self.end_test_step() + self.end_test_case() + + def _ensure_clean_workspace_step(self): + + # Delete any active OIR we might own + test_step_fragments.cleanup_active_oirs( + self, + self._dss, + self._planning_area_volume4d.to_f3548v21(), + self._expected_manager, + ) + + # Make sure the OIR ID we are going to use is available + test_step_fragments.cleanup_op_intent(self, self._dss, self._oir_id) + # Start by dropping any active subs we might own and that could interfere + test_step_fragments.cleanup_active_subs( + self, self._dss, self._planning_area_volume4d.to_f3548v21() + ) + + def _create_oir_with_params( + self, creation_params: PutOperationalIntentReferenceParameters + ): + + with self.check( + "Create operational intent reference query succeeds", [self._primary_pid] + ) as check: + try: + oir, subs, q = self._dss.put_op_intent( + extents=creation_params.extents, + key=creation_params.key, + state=creation_params.state, + base_url=creation_params.uss_base_url, + oi_id=self._oir_id, + ovn=None, + ) + self.record_query(q) + except QueryError as qe: + self.record_queries(qe.queries) + check.record_failed( + summary="Create operational intent reference failed", + details=qe.msg, + query_timestamps=qe.query_timestamps, + ) + return + + with self.check( + "Create operational intent reference response is correct", + [self._primary_pid], + ) as check: + OIRValidator( + main_check=check, + scenario=self, + expected_manager=self._expected_manager, + participant_id=[self._primary_pid], + oir_params=creation_params, + ).validate_created_oir(self._oir_id, new_oir=q) + + self._current_oir = oir + + def _query_secondaries_and_compare( + self, expected_oir_params: PutOperationalIntentReferenceParameters + ): + for secondary_dss in self._dss_read_instances: + self._validate_oir_from_secondary( + secondary_dss=secondary_dss, + expected_oir_params=expected_oir_params, + involved_participants=list( + {self._primary_pid, secondary_dss.participant_id} + ), + ) + + def _validate_oir_from_secondary( + self, + secondary_dss: DSSInstance, + expected_oir_params: PutOperationalIntentReferenceParameters, + involved_participants: List[str], + ): + with self.check( + "Operational intent reference can be found at every DSS", + involved_participants, + ) as check: + try: + oir, q = secondary_dss.get_op_intent_reference(self._oir_id) + self.record_query(q) + except QueryError as e: + self.record_queries(e.queries) + check.record_failed( + summary="GET for operational intent reference failed", + details=f"Query for operational intent reference failed: {e.msg}", + query_timestamps=e.query_timestamps, + ) + + with self.check( + "Propagated operational intent reference contains the correct manager", + involved_participants, + ) as check: + if oir.manager != self._expected_manager: + check.record_failed( + summary="Propagated OIR has an incorrect manager", + details=f"Expected: {self._expected_manager}, Received: {oir.manager}", + query_timestamps=[q.request.timestamp], + ) + + with self.check( + "Propagated operational intent reference contains the correct USS base URL", + involved_participants, + ) as check: + if oir.uss_base_url != expected_oir_params.uss_base_url: + check.record_failed( + "Propagated OIR has an incorrect USS base URL", + details=f"Expected: {expected_oir_params.base_url}, Received: {oir.uss_base_url}", + query_timestamps=[q.request.timestamp], + ) + + with self.check( + "Propagated operational intent reference contains the correct state", + involved_participants, + ) as check: + if oir.state != expected_oir_params.state: + check.record_failed( + summary="Propagated OIR has an incorrect state", + details=f"Expected: {expected_oir_params.state}, Received: {oir.state}", + query_timestamps=[q.request.timestamp], + ) + + with self.check( + "Propagated operational intent reference contains the correct start time", + involved_participants, + ) as check: + expected_start = expected_oir_params.extents[0].time_start + if ( + abs( + oir.time_start.value.datetime - expected_start.value.datetime + ).total_seconds() + > TIME_TOLERANCE_SEC + ): + check.record_failed( + "Propagated OIR has an incorrect start time", + details=f"Expected: {expected_start}, Received: {oir.time_start}", + query_timestamps=[q.request.timestamp], + ) + + with self.check( + "Propagated operational intent reference contains the correct end time", + involved_participants, + ) as check: + expected_end = expected_oir_params.extents[-1].time_end + if ( + abs( + oir.time_end.value.datetime - expected_end.value.datetime + ).total_seconds() + > TIME_TOLERANCE_SEC + ): + check.record_failed( + "Propagated OIR has an incorrect end time", + details=f"Expected: {expected_end}, Received: {oir.time_end}", + query_timestamps=[q.request.timestamp], + ) + + with self.check( + "Operational intent reference returned by a secondary DSS is valid and correct", + [secondary_dss.participant_id], + ) as check: + # Do a full validation of the OIR as a sanity check + OIRValidator( + main_check=check, + scenario=self, + expected_manager=self._expected_manager, + participant_id=[secondary_dss.participant_id], + oir_params=expected_oir_params, + ).validate_fetched_oir( + expected_oir_id=self._oir_id, + fetched_oir=q, + expected_version=self._current_oir.version, + expected_ovn=self._current_oir.ovn, + ) + + def _test_mutate_oir_shift_time(self): + """Mutate the OIR by adding 10 seconds to its start and end times. + This is achieved by updating the first and last element of the extents. + """ + op = self._oir_params + + new_extents = self._shift_extents(op.extents, timedelta(seconds=10)) + + new_params = PutOperationalIntentReferenceParameters( + extents=new_extents, + key=op.key + [self._current_oir.ovn], + state=op.state, + uss_base_url=op.uss_base_url, + subscription_id=op.subscription_id if "subscription_id" in op else None, + new_subscription=op.new_subscription if "new_subscription" in op else None, + ) + + with self.check( + "Mutate operational intent reference query succeeds", [self._primary_pid] + ) as check: + try: + oir, subs, q = self._dss.put_op_intent( + extents=new_extents, + key=new_params.key, + state=new_params.state, + base_url=new_params.uss_base_url, + oi_id=self._oir_id, + ovn=self._current_oir.ovn, + ) + self.record_query(q) + except QueryError as qe: + self.record_queries(qe.queries) + check.record_failed( + summary="Operational intent reference mutation failed", + details=qe.msg, + query_timestamps=qe.query_timestamps, + ) + return + + with self.check( + "Mutate operational intent reference query succeeds", + [self._primary_pid], + ) as check: + OIRValidator( + main_check=check, + scenario=self, + expected_manager=self._expected_manager, + participant_id=[self._primary_pid], + oir_params=new_params, + ).validate_mutated_oir( + expected_oir_id=self._oir_id, + mutated_oir=q, + previous_ovn=self._current_oir.ovn, + previous_version=self._current_oir.version, + ) + + self._oir_params = new_params + self._current_oir = oir + + def _test_delete_sub(self): + with self.check( + "Delete operational intent reference query succeeds", [self._primary_pid] + ) as check: + try: + oir, subs, q = self._dss.delete_op_intent( + self._oir_id, self._current_oir.ovn + ) + self.record_query(q) + except QueryError as qe: + self.record_queries(qe.queries) + check.record_failed( + summary="Operational intent reference deletion on primary DSS failed", + details=qe.msg, + query_timestamps=qe.query_timestamps, + ) + return + + with self.check( + "Delete operational intent reference response is correct", + [self._primary_pid], + ) as check: + OIRValidator( + main_check=check, + scenario=self, + expected_manager=self._expected_manager, + participant_id=[self._primary_pid], + oir_params=self._oir_params, + ).validate_deleted_oir( + expected_oir_id=self._oir_id, + deleted_oir=q, + expected_ovn=self._current_oir.ovn, + expected_version=self._current_oir.version, + ) + + self._current_oir = None + + def _test_get_deleted_oir(self): + for secondary_dss in self._dss_read_instances: + self._confirm_secondary_has_no_oir(secondary_dss) + + def _confirm_secondary_has_no_oir(self, secondary_dss: DSSInstance): + with self.check( + "Secondary DSS should not return the deleted operational intent reference", + [secondary_dss.participant_id], + ) as check: + try: + oir, q = secondary_dss.get_op_intent_reference(self._oir_id) + self.record_query(q) + status = q.status_code + q_ts = [q.request.timestamp] + except QueryError as qe: + status = qe.last_status_code + q_ts = qe.query_timestamps[-1] + if status != 404: + check.record_failed( + "Secondary DSS still has the deleted operational intent reference", + details=f"Expected 404, received {status}", + query_timestamps=q_ts, + ) + + def _shift_extents( + self, extents: List[api.Volume4D], delta: timedelta + ) -> List[api.Volume4D]: + return [ + api.Volume4D( + volume=ext.volume, + time_start=api.Time( + value=StringBasedDateTime(ext.time_start.value.datetime + delta) + ), + time_end=api.Time( + value=StringBasedDateTime(ext.time_end.value.datetime + delta) + ), + ) + for ext in extents + ] + + def cleanup(self): + self.begin_cleanup() + self._ensure_clean_workspace_step() + self.end_cleanup() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/test_step_fragments.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/test_step_fragments.py index 1cf30767cf..4b108bc904 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/test_step_fragments.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/test_step_fragments.py @@ -2,6 +2,7 @@ import loguru +from monitoring.monitorlib.fetch import QueryError from monitoring.monitorlib.mutate.scd import MutatedSubscription from monitoring.monitorlib import fetch from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance @@ -19,7 +20,8 @@ def remove_op_intent( This function implements the test step fragment described in remove_op_intent.md. """ with scenario.check( - "Operational intent reference removed", dss.participant_id + "Operational intent references can be deleted by their owner", + dss.participant_id, ) as check: try: removed_ref, subscribers_to_notify, query = dss.delete_op_intent(oi_id, ovn) @@ -36,6 +38,30 @@ def remove_op_intent( # TODO: Attempt to notify subscribers +def cleanup_op_intent( + scenario: TestScenarioType, dss: DSSInstance, oi_id: EntityID +) -> None: + """Remove the specified operational intent reference from the DSS, if it exists.""" + + with scenario.check( + "Operational intent references can be queried by ID", [dss.participant_id] + ) as check: + try: + oir, q = dss.get_op_intent_reference(oi_id) + except fetch.QueryError as e: + scenario.record_queries(e.queries) + if e.last_status_code != 404: + check.record_failed( + summary="OIR Get query returned code different from 200 or 404", + details=e.msg, + query_timestamps=e.query_timestamps, + ) + else: + return + + remove_op_intent(scenario, dss, oi_id, oir.ovn) + + def cleanup_sub( scenario: TestScenarioType, dss: DSSInstance, sub_id: EntityID ) -> Optional[MutatedSubscription]: @@ -64,7 +90,7 @@ def cleanup_sub( if deleted_sub.status_code != 200: check.record_failed( summary=f"Could not delete subscription {sub_id}", - details=f"When attempting to delete subscription {sub_id} from the DSS, received {deleted_sub.status_code}", + details=f"When attempting to delete subscription {sub_id} from the DSS, received status {deleted_sub.status_code}: {deleted_sub.response.json}", query_timestamps=[deleted_sub.request.timestamp], ) @@ -92,3 +118,23 @@ def cleanup_active_subs( for sub_id in query.subscriptions.keys(): cleanup_sub(scenario, dss, sub_id) + + +def cleanup_active_oirs( + scenario: TestScenarioType, + dss: DSSInstance, + volume: Volume4D, + manager_identity: str, +) -> None: + with scenario.check( + "Operational intent references can be searched for", [dss.participant_id] + ) as check: + try: + oirs, query = dss.find_op_intent(volume) + except QueryError as qe: + scenario.record_queries(qe.queries) + raise qe + + for oir in oirs: + if oir.manager == manager_identity: + remove_op_intent(scenario, dss, oir.id, oir.ovn) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/validators/oir_validator.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/validators/oir_validator.py new file mode 100644 index 0000000000..f6b1bfc7af --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/validators/oir_validator.py @@ -0,0 +1,505 @@ +from datetime import datetime +from typing import Optional, List + +from implicitdict import ImplicitDict +from uas_standards.astm.f3548.v21.api import ( + PutOperationalIntentReferenceParameters, + EntityID, + OperationalIntentReference, + ChangeOperationalIntentReferenceResponse, + EntityOVN, + GetOperationalIntentReferenceResponse, + QueryOperationalIntentReferenceResponse, +) + +from monitoring.monitorlib import schema_validation, fetch +from monitoring.monitorlib.schema_validation import F3548_21 +from monitoring.uss_qualifier.scenarios.scenario import PendingCheck, TestScenario + +TIME_TOLERANCE_SEC = 1 +"""tolerance when comparing created vs returned timestamps""" + + +class OIRValidator: + """ + Wraps the validation logic for an operational intent reference that was returned by a DSS + + It will compare the provided OIR with the parameters specified at its creation. + """ + + _main_check: PendingCheck + """ + The overarching check corresponding to the general validation of an OIR. + This check will be failed if any of the sub-checks carried out by this validator fail. + """ + + _scenario: TestScenario + """ + Scenario in which this validator is being used. Will be used to register checks. + """ + + _oir_params: Optional[PutOperationalIntentReferenceParameters] + _pid: List[str] + """Participant ID(s) to use for the checks""" + + def __init__( + self, + main_check: PendingCheck, + scenario: TestScenario, + expected_manager: str, + participant_id: List[str], + oir_params: Optional[PutOperationalIntentReferenceParameters], + ): + self._main_check = main_check + self._scenario = scenario + self._pid = participant_id + self._oir_params = oir_params + self._expected_manager = expected_manager + self._expected_start = oir_params.extents[0].time_start.value.datetime + self._expected_end = oir_params.extents[-1].time_end.value.datetime + + def _fail_sub_check( + self, sub_check: PendingCheck, summary: str, details: str, t_dss: datetime + ) -> None: + """ + Fail the passed sub check with the passed summary and details, and fail + the main check with the passed details. + + The provided timestamp is forwarded into the query_timestamps of the check failure. + """ + sub_check.record_failed( + summary=summary, + details=details, + query_timestamps=[t_dss], + ) + + self._main_check.record_failed( + summary=f"Invalid OIR returned by the DSS: {summary}", + details=details, + query_timestamps=[t_dss], + ) + + def _fail_with_schema_errors( + self, + check: PendingCheck, + errors: List[schema_validation.ValidationError], + t_dss: datetime, + ) -> None: + """Fail the passed check with the passed schema validation errors, and fail + the main check with the passed details.""" + details = "\n".join(f"[{e.json_path}] {e.message}" for e in errors) + self._fail_sub_check( + check, + summary="Response format was invalid", + details="Found the following schema validation errors in the DSS response:\n" + + details, + t_dss=t_dss, + ) + + def _validate_oir( + self, + expected_entity_id: EntityID, + dss_oir: OperationalIntentReference, + t_dss: datetime, + previous_version: Optional[int], + expected_version: Optional[int], + previous_ovn: Optional[str], + expected_ovn: Optional[str], + ) -> None: + """ + Args: + expected_entity_id: the ID we expect to find in the entity + dss_oir: the OIR returned by the DSS + t_dss: timestamp of the query to the DSS for failure reporting + previous_ovn: previous OVN of the entity, if we are verifying a mutation + expected_ovn: expected OVN of the entity, if we are verifying a read query + previous_version: previous version of the entity, if we are verifying a mutation + expected_version: expected version of the entity, if we are verifying a read query + """ + + with self._scenario.check( + "Returned operational intent reference ID is correct", self._pid + ) as check: + if dss_oir.id != expected_entity_id: + self._fail_sub_check( + check, + summary=f"Returned OIR ID is incorrect", + details=f"Expected OIR ID {expected_entity_id}, got {dss_oir.id}", + t_dss=t_dss, + ) + + with self._scenario.check( + "Returned operational intent reference has a manager", self._pid + ) as check: + # Check for empty string. None should have failed the schema check earlier + if not dss_oir.manager: + self._fail_sub_check( + check, + summary="No OIR manager was specified", + details=f"Expected: {self._expected_manager}, got an empty or undefined string", + t_dss=t_dss, + ) + + with self._scenario.check( + "Returned operational intent reference manager is correct", self._pid + ) as check: + if dss_oir.manager != self._expected_manager: + self._fail_sub_check( + check, + summary="Returned manager is incorrect", + details=f"Expected. {self._expected_manager}, got {dss_oir.manager}", + t_dss=t_dss, + ) + + with self._scenario.check( + "Returned operational intent reference has an USS base URL", self._pid + ) as check: + # If uss_base_url is not present, or it is None or Empty, we should fail: + if "uss_base_url" not in dss_oir or not dss_oir.uss_base_url: + self._fail_sub_check( + check, + summary="Returned OIR has no USS base URL", + details="The OIR returned by the DSS has no USS base URL when it should have one", + t_dss=t_dss, + ) + + with self._scenario.check( + "Returned operational intent reference base URL is correct", self._pid + ) as check: + if dss_oir.uss_base_url != self._oir_params.uss_base_url: + self._fail_sub_check( + check, + summary="Returned USS Base URL does not match provided one", + details=f"Provided: {self._oir_params.uss_base_url}, Returned: {dss_oir.uss_base_url}", + t_dss=t_dss, + ) + + with self._scenario.check( + "Returned operational intent reference has a start time", self._pid + ) as check: + if "time_start" not in dss_oir or dss_oir.time_start is None: + self._fail_sub_check( + check, + summary="Returned OIR has no start time", + details="The operational intent reference returned by the DSS has no start time when it should have one", + t_dss=t_dss, + ) + + with self._scenario.check( + "Returned operational intent reference has an end time", self._pid + ) as check: + if "time_end" not in dss_oir or dss_oir.time_end is None: + self._fail_sub_check( + check, + summary="Returned OIR has no end time", + details="The operational intent reference returned by the DSS has no end time when it should have one", + t_dss=t_dss, + ) + + with self._scenario.check("Returned start time is correct", self._pid) as check: + if ( + abs( + dss_oir.time_start.value.datetime - self._expected_start + ).total_seconds() + > TIME_TOLERANCE_SEC + ): + self._fail_sub_check( + check, + summary="Returned start time does not match provided one", + details=f"Provided: {self._oir_params.start_time}, Returned: {dss_oir.time_start}", + t_dss=t_dss, + ) + + with self._scenario.check("Returned end time is correct", self._pid) as check: + if ( + abs( + dss_oir.time_end.value.datetime - self._expected_end + ).total_seconds() + > TIME_TOLERANCE_SEC + ): + self._fail_sub_check( + check, + summary="Returned end time does not match provided one", + details=f"Provided: {self._oir_params.end_time}, Returned: {dss_oir.time_end}", + t_dss=t_dss, + ) + + # If the previous OVN is not None, we are dealing with a mutation: + if previous_ovn is not None: + with self._scenario.check( + "Mutated operational intent reference OVN is updated", self._pid + ) as check: + if dss_oir.ovn == previous_ovn: + self._fail_sub_check( + check, + summary="Returned OIR OVN was not updated", + details=f"Expected OVN to be different from {previous_ovn}, but it was not", + t_dss=t_dss, + ) + + if expected_ovn is not None: + with self._scenario.check( + "Non-mutated operational intent reference keeps the same OVN", self._pid + ) as check: + if dss_oir.ovn != expected_ovn: + self._fail_sub_check( + check, + summary="Returned OIR OVN was updated", + details=f"Expected OVN to be {expected_ovn}, Returned: {dss_oir.ovn}", + t_dss=t_dss, + ) + + # If the previous version is not None, we are dealing with a mutation: + if previous_version is not None: + with self._scenario.check( + "Mutated operational intent reference version is updated", self._pid + ) as check: + # TODO confirm that a mutation should imply a version update + if dss_oir.version == previous_version: + self._fail_sub_check( + check, + summary="Returned OIR version was not updated", + details=f"Expected version to be different from {previous_ovn}, but it was not", + t_dss=t_dss, + ) + + # TODO version _might_ get incremented due to changes caused outside of the uss_qualifier + # and we should probably check if it is equal or higher. + if expected_version is not None: + with self._scenario.check( + "Non-mutated operational intent reference keeps the same version", + self._pid, + ) as check: + if dss_oir.version != expected_version: + self._fail_sub_check( + check, + summary="Returned OIR version was updated", + details=f"Expected version to be {expected_ovn}, Returned: {dss_oir.version}", + t_dss=t_dss, + ) + + # TODO add check for: + # - state + # - subscription ID of the OIR (based on passed parameters, if these were set) + + def _validate_put_oir_response_schema( + self, oir_query: fetch.Query, t_dss: datetime, action: str + ): + """Validate response bodies for creation and mutation of OIRs.""" + + check_name = ( + "Create operational intent reference response format conforms to spec" + if action == "create" + else "Mutate operational intent reference response format conforms to spec" + ) + + with self._scenario.check(check_name, self._pid) as check: + errors = schema_validation.validate( + F3548_21.OpenAPIPath, + F3548_21.ChangeOperationalIntentReferenceResponse, + oir_query.response.json, + ) + if errors: + self._fail_with_schema_errors(check, errors, t_dss) + + def validate_created_oir( + self, expected_oir_id: EntityID, new_oir: fetch.Query + ) -> None: + """Validate an OIR that was just explicitly created, meaning + we don't have a previous version to compare to, and we expect it to not be an implicit one.""" + + t_dss = new_oir.request.timestamp + + # Validate the response schema + self._validate_put_oir_response_schema(new_oir, t_dss, "create") + + # TODO do we want to add a wrapper type? We should check the schema before parsing? + # Otherwise we run into exceptions before actually checking the schema? + # Expected to pass given that we validated the JSON against the schema + parsed_resp = ImplicitDict.parse( + new_oir.response.json, ChangeOperationalIntentReferenceResponse + ) + + oir: OperationalIntentReference = parsed_resp.operational_intent_reference + + # Validate the OIR itself + self._validate_oir( + expected_entity_id=expected_oir_id, + dss_oir=oir, + t_dss=t_dss, + previous_version=None, + expected_version=None, + previous_ovn=None, + expected_ovn=None, + ) + + def validate_mutated_oir( + self, + expected_oir_id: EntityID, + mutated_oir: fetch.Query, + previous_ovn: str, + previous_version: int, + ) -> None: + """Validate an OIR that was just mutated, meaning we have a previous version and OVN to compare to. + Callers must specify if this is an implicit OIR or not.""" + t_dss = mutated_oir.request.timestamp + + # Validate the response schema + self._validate_put_oir_response_schema(mutated_oir, t_dss, "mutate") + + oir = ImplicitDict.parse( + mutated_oir.response.json, ChangeOperationalIntentReferenceResponse + ).operational_intent_reference + + # Validate the OIR itself + self._validate_oir( + expected_entity_id=expected_oir_id, + dss_oir=oir, + t_dss=t_dss, + previous_version=previous_version, + expected_version=None, + previous_ovn=previous_ovn, + expected_ovn=None, + ) + + def validate_fetched_oir( + self, + expected_oir_id: EntityID, + fetched_oir: fetch.Query, + expected_version: int, + expected_ovn: EntityOVN, + ) -> None: + """Validate an OIR that was directly queried by its ID.""" + + t_dss = fetched_oir.request.timestamp + + # Validate the response schema + with self._scenario.check( + "Get operational intent reference response format conforms to spec", + self._pid, + ) as check: + errors = schema_validation.validate( + F3548_21.OpenAPIPath, + F3548_21.GetOperationalIntentReferenceResponse, + fetched_oir.response.json, + ) + if errors: + self._fail_with_schema_errors(check, errors, t_dss) + + parsed_resp = fetched_oir.parse_json_result( + GetOperationalIntentReferenceResponse + ) + # Validate the OIR itself + self._validate_oir( + expected_entity_id=expected_oir_id, + dss_oir=parsed_resp.operational_intent_reference, + t_dss=t_dss, + previous_version=None, + expected_version=expected_version, + previous_ovn=None, + expected_ovn=expected_ovn, + ) + + def validate_searched_oir( + self, + expected_oir_id: EntityID, + search_response: fetch.Query, + expected_ovn: str, + expected_version: int, + ) -> None: + """Validate an OIR that was retrieved through search. + Note that the callers need to pass the entire response from the DSS, as the schema check + will be performed on the entire response, not just the OIR itself. + However, only the expected OIR is checked for the correctness of its contents.""" + + t_dss = search_response.request.timestamp + + # Validate the response schema + self.validate_searched_oir_format(search_response, t_dss) + + resp_parsed = search_response.parse_json_result( + QueryOperationalIntentReferenceResponse + ) + + by_id = {oir: oir.id for oir in resp_parsed.operational_intent_references} + + with self._scenario.check( + "Created operational intent reference is in search results", self._pid + ) as check: + if expected_oir_id not in by_id: + self._fail_sub_check( + check, + summary="Created OIR is not present in search results", + details=f"The OIR with ID {expected_oir_id} was expected to be found in the search results, but these only contained the following entities: {by_id.keys()}", + t_dss=t_dss, + ) + # Depending on the severity defined in the documentation, the above might not raise an exception, + # and we should still stop here if the check failed. + return + + oir = by_id[expected_oir_id] + + # Validate the OIR itself + self._validate_oir( + expected_entity_id=expected_oir_id, + dss_oir=oir, + t_dss=t_dss, + previous_ovn=None, + expected_ovn=expected_ovn, + previous_version=None, + expected_version=expected_version, + ) + + def validate_searched_oir_format( + self, search_response: fetch.Query, t_dss: datetime + ) -> None: + # Validate the response schema + with self._scenario.check( + "Search operational intent reference response format conforms to spec", + self._pid, + ) as check: + errors = schema_validation.validate( + F3548_21.OpenAPIPath, + F3548_21.QueryOperationalIntentReferenceResponse, + search_response.response.json, + ) + if errors: + self._fail_with_schema_errors(check, errors, t_dss) + + def validate_deleted_oir( + self, + expected_oir_id: EntityID, + deleted_oir: fetch.Query, + expected_ovn: str, + expected_version: int, + ) -> None: + + t_dss = deleted_oir.request.timestamp + + # Validate the response schema + with self._scenario.check( + "Delete operational intent reference response format conforms to spec", + self._pid, + ) as check: + errors = schema_validation.validate( + F3548_21.OpenAPIPath, + F3548_21.ChangeOperationalIntentReferenceResponse, + deleted_oir.response.json, + ) + if errors: + self._fail_with_schema_errors(check, errors, t_dss) + + oir_resp = deleted_oir.parse_json_result( + ChangeOperationalIntentReferenceResponse + ) + + # Validate the OIR itself + self._validate_oir( + expected_entity_id=expected_oir_id, + dss_oir=oir_resp.operational_intent_reference, + t_dss=t_dss, + previous_ovn=None, + expected_ovn=expected_ovn, + previous_version=None, + expected_version=expected_version, + ) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md index 105eab423d..f897b7c6ac 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md @@ -80,7 +80,7 @@ In addition to foreign flight planners, uss_qualifier may have left operational ### Remove uss_qualifier op intents test step -#### [Remove op intents](./dss/remove_op_intent.md) +#### [Remove op intents](./dss/clean_workspace.md) The operational intent references managed by uss_qualifier discovered in the previous test case are removed. diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.yaml b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.yaml index 2beaa7178e..470c7a4ea1 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.yaml +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.yaml @@ -28,6 +28,7 @@ actions: utm_client_identity: utm_client_identity id_generator: id_generator isa: service_area + client_identity: utm_client_identity problematically_big_area: problematically_big_area on_failure: Continue dss_instances_source: dss_instances diff --git a/monitoring/uss_qualifier/suites/astm/utm/dss_probing.md b/monitoring/uss_qualifier/suites/astm/utm/dss_probing.md index b2810105d7..6308eda912 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/dss_probing.md +++ b/monitoring/uss_qualifier/suites/astm/utm/dss_probing.md @@ -4,11 +4,12 @@ ## [Actions](../../README.md#actions) -1. Scenario: [ASTM SCD DSS: Subscription Simple](../../../scenarios/astm/utm/dss/subscription_simple.md) ([`scenarios.astm.utm.dss.SubscriptionSimple`](../../../scenarios/astm/utm/dss/subscription_simple.py)) -2. Scenario: [ASTM SCD DSS: Subscription Validation](../../../scenarios/astm/utm/dss/subscription_validation.md) ([`scenarios.astm.utm.dss.SubscriptionValidation`](../../../scenarios/astm/utm/dss/subscription_validation.py)) -3. Scenario: [ASTM F3548-21 UTM DSS Operational Intent Reference Access Control](../../../scenarios/astm/utm/op_intent_ref_access_control.md) ([`scenarios.astm.utm.OpIntentReferenceAccessControl`](../../../scenarios/astm/utm/op_intent_ref_access_control.py)) -4. Scenario: [ASTM F3548-21 UTM DSS interoperability](../../../scenarios/astm/utm/dss_interoperability.md) ([`scenarios.astm.utm.DSSInteroperability`](../../../scenarios/astm/utm/dss_interoperability.py)) -5. Scenario: [ASTM SCD DSS: Subscription Synchronization](../../../scenarios/astm/utm/dss/synchronization/subscription_synchronization.md) ([`scenarios.astm.utm.dss.synchronization.SubscriptionSynchronization`](../../../scenarios/astm/utm/dss/synchronization/subscription_synchronization.py)) +1. Scenario: [ASTM SCD DSS: Operational Intent Reference Synchronization](../../../scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.md) ([`scenarios.astm.utm.dss.synchronization.OIRSynchronization`](../../../scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py)) +2. Scenario: [ASTM SCD DSS: Subscription Simple](../../../scenarios/astm/utm/dss/subscription_simple.md) ([`scenarios.astm.utm.dss.SubscriptionSimple`](../../../scenarios/astm/utm/dss/subscription_simple.py)) +3. Scenario: [ASTM SCD DSS: Subscription Validation](../../../scenarios/astm/utm/dss/subscription_validation.md) ([`scenarios.astm.utm.dss.SubscriptionValidation`](../../../scenarios/astm/utm/dss/subscription_validation.py)) +4. Scenario: [ASTM F3548-21 UTM DSS Operational Intent Reference Access Control](../../../scenarios/astm/utm/op_intent_ref_access_control.md) ([`scenarios.astm.utm.OpIntentReferenceAccessControl`](../../../scenarios/astm/utm/op_intent_ref_access_control.py)) +5. Scenario: [ASTM F3548-21 UTM DSS interoperability](../../../scenarios/astm/utm/dss_interoperability.md) ([`scenarios.astm.utm.DSSInteroperability`](../../../scenarios/astm/utm/dss_interoperability.py)) +6. Scenario: [ASTM SCD DSS: Subscription Synchronization](../../../scenarios/astm/utm/dss/synchronization/subscription_synchronization.md) ([`scenarios.astm.utm.dss.synchronization.SubscriptionSynchronization`](../../../scenarios/astm/utm/dss/synchronization/subscription_synchronization.py)) ## [Checked requirements](../../README.md#checked-requirements) @@ -20,20 +21,20 @@