From 491563e376d5f18a6a9184a4b0c53b34b10a7b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Misbach?= Date: Wed, 13 Mar 2024 12:46:48 +0100 Subject: [PATCH 1/3] [uss_qualifier/utm] Add make_report test step fragment --- .../resources/astm/f3548/v21/dss.py | 62 +++++++++++++++++++ .../scenarios/astm/utm/make_dss_report.md | 10 +++ .../scenarios/astm/utm/test_steps.py | 40 ++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 monitoring/uss_qualifier/scenarios/astm/utm/make_dss_report.md diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py index 530de8e116..bfc139559b 100644 --- a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py +++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py @@ -30,6 +30,8 @@ OperationID, GetOperationalIntentTelemetryResponse, VehicleTelemetry, + ExchangeRecord, + ErrorReport, ) from uas_standards.astm.f3548.v21.constants import Scope @@ -104,6 +106,12 @@ def _uses_scope(self, *scopes: Tuple[str]) -> None: f"{fullname(type(self))} client called {calling_function_name(1)} which requires the use of the scope `{scope}`, but this DSSInstance is only authorized to perform actions with the scopes {' or '.join(self._scopes_authorized)}" ) + def _uses_any_scope(self, *scopes: str) -> None: + if not any([scope in self._scopes_authorized for scope in scopes]): + raise ValueError( + f"{fullname(type(self))} client called {calling_function_name(1)} which requires the use of any of the scopes `{', '.join(scopes)}`, but this DSSInstance is only authorized to perform actions with the scopes {' or '.join(self._scopes_authorized)}" + ) + def can_use_scope(self, scope: str) -> bool: return scope in self._scopes_authorized @@ -369,6 +377,60 @@ def set_uss_availability( result = query.parse_json_result(UssAvailabilityStatusResponse) return result.version, query + def make_report( + self, + exchange: ExchangeRecord, + ) -> Tuple[Optional[str], Query]: + """ + Make a DSS report. + Returns: + A tuple composed of + 1) the report ID; + 2) the query. + Raises: + * QueryError: if request failed, if HTTP status code is different than 201, or if the parsing of the response failed. + """ + self._uses_any_scope( + Scope.ConstraintManagement, + Scope.ConstraintProcessing, + Scope.StrategicCoordination, + Scope.ConformanceMonitoringForSituationalAwareness, + Scope.AvailabilityArbitration, + ) + + req = ErrorReport(exchange=exchange) + op = OPERATIONS[OperationID.MakeDssReport] + query = query_and_describe( + self.client, + op.verb, + op.path, + QueryType.F3548v21DSSMakeDssReport, + self.participant_id, + scope=next( + iter(self._scopes_authorized) + ), # any scope is valid for this endpoint + json=req, + ) + + # TODO: this is a temporary hack: the endpoint is currently not implemented in the DSS, as such we expect the + # DSS to respond with a 400 and a specific error message. This must be updated once this endpoint is actually + # implemented in the DSS. + # if query.status_code != 201: + # raise QueryError( + # f"Received code {query.status_code} when attempting to make DSS report{f'; error message: `{query.error_message}`' if query.error_message is not None else ''}", + # query, + # ) + # else: + # result = query.parse_json_result(ErrorReport) + # return result.report_id, query + if query.status_code != 400 or "Not yet implemented" not in query.error_message: + raise QueryError( + f"Received code {query.status_code} when attempting to make DSS report{f'; error message: `{query.error_message}`' if query.error_message is not None else ''}", + query, + ) + else: + return "dummy_report_id", query + def query_subscriptions( self, volume: Volume4D, diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/make_dss_report.md b/monitoring/uss_qualifier/scenarios/astm/utm/make_dss_report.md new file mode 100644 index 0000000000..5b66df538a --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/make_dss_report.md @@ -0,0 +1,10 @@ +# Make report to DSS test step fragment +This step makes a report to the DSS. + +See `make_dss_report` in [test_steps.py](test_steps.py). + +## 🛑 DSS report successfully submitted check +If the submission of the report to the DSS does not succeed, this check will fail per **[astm.f3548.v21.DSS0100,2](../../../requirements/astm/f3548/v21.md)**. + +## ⚠️ DSS returned a valid report ID check +If the ID returned by the DSS is not present or is empty, this check will fail per **[astm.f3548.v21.DSS0100,2](../../../requirements/astm/f3548/v21.md)**. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py index d889bb68b0..ea01819ea8 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py @@ -11,6 +11,7 @@ OperationalIntentReference, GetOperationalIntentDetailsResponse, EntityID, + ExchangeRecord, ) from uas_standards.astm.f3548.v21.constants import Scope from monitoring.monitorlib.clients.flight_planning.flight_info import ( @@ -698,3 +699,42 @@ def set_uss_down( query_timestamps=[avail_query.request.timestamp], ) return availability_version + + +def make_report( + scenario: TestScenarioType, + dss: DSSInstance, + exchange: ExchangeRecord, +) -> str: + """Make a DSS report. + + This function implements the test step fragment described in make_dss_report.md. + + Returns: + The report ID. + """ + with scenario.check( + "DSS report successfully submitted", [dss.participant_id] + ) as check: + try: + report_id, report_query = dss.make_report(exchange) + scenario.record_query(report_query) + except QueryError as e: + scenario.record_queries(e.queries) + report_query = e.cause + check.record_failed( + summary="DSS report could not be submitted", + details=f"DSS responded code {report_query.status_code}; {e}", + query_timestamps=[report_query.request.timestamp], + ) + + with scenario.check( + "DSS returned a valid report ID", [dss.participant_id] + ) as check: + if report_id is None or len(report_id) == 0: + check.record_failed( + summary="Submitted DSS report returned no or empty ID", + details=f"DSS responded code {report_query.status_code} but with no ID for the report", + query_timestamps=[report_query.request.timestamp], + ) + return report_id From a9c2a6d2d510900d149126c1edc99df1001f32ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Misbach?= Date: Tue, 19 Mar 2024 14:41:20 +0100 Subject: [PATCH 2/3] ensure use of a valid scope for request --- .../resources/astm/f3548/v21/dss.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py index bfc139559b..b0bc9fbef1 100644 --- a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py +++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py @@ -106,11 +106,15 @@ def _uses_scope(self, *scopes: Tuple[str]) -> None: f"{fullname(type(self))} client called {calling_function_name(1)} which requires the use of the scope `{scope}`, but this DSSInstance is only authorized to perform actions with the scopes {' or '.join(self._scopes_authorized)}" ) - def _uses_any_scope(self, *scopes: str) -> None: - if not any([scope in self._scopes_authorized for scope in scopes]): - raise ValueError( - f"{fullname(type(self))} client called {calling_function_name(1)} which requires the use of any of the scopes `{', '.join(scopes)}`, but this DSSInstance is only authorized to perform actions with the scopes {' or '.join(self._scopes_authorized)}" - ) + def _uses_any_scope(self, *scopes: str) -> str: + """Validates that at least a required scope is authorized for a request. + Additionally, returns a valid scope that may be used for the request.""" + for scope in scopes: + if scope in self._scopes_authorized: + return scope + raise ValueError( + f"{fullname(type(self))} client called {calling_function_name(1)} which requires the use of any of the scopes `{', '.join(scopes)}`, but this DSSInstance is only authorized to perform actions with the scopes {' or '.join(self._scopes_authorized)}" + ) def can_use_scope(self, scope: str) -> bool: return scope in self._scopes_authorized @@ -390,7 +394,7 @@ def make_report( Raises: * QueryError: if request failed, if HTTP status code is different than 201, or if the parsing of the response failed. """ - self._uses_any_scope( + use_scope = self._uses_any_scope( Scope.ConstraintManagement, Scope.ConstraintProcessing, Scope.StrategicCoordination, @@ -406,9 +410,7 @@ def make_report( op.path, QueryType.F3548v21DSSMakeDssReport, self.participant_id, - scope=next( - iter(self._scopes_authorized) - ), # any scope is valid for this endpoint + scope=use_scope, json=req, ) From 6859e05c4f7280855a427f7ba2e82447ae8e96a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Misbach?= Date: Tue, 19 Mar 2024 14:45:51 +0100 Subject: [PATCH 3/3] simplify --- monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py index ea01819ea8..2adaf0ac3a 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py @@ -731,7 +731,7 @@ def make_report( with scenario.check( "DSS returned a valid report ID", [dss.participant_id] ) as check: - if report_id is None or len(report_id) == 0: + if not report_id: check.record_failed( summary="Submitted DSS report returned no or empty ID", details=f"DSS responded code {report_query.status_code} but with no ID for the report",