From cb113e2bd96ca0f3b765958b4730d4f7c07e5bf7 Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Wed, 29 Jul 2026 08:51:29 -0600 Subject: [PATCH] Fix schema validation in unit tests Update orchestration validation for every current action payload, add entity-state validation, and restore all disabled schema assertions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9dbcaef-d089-440a-81ea-6db94166cad1 --- tests/orchestrator/orchestrator_test_utils.py | 8 + .../orchestrator/schemas/EntityStateSchema.py | 43 ++++ .../schemas/OrchetrationStateSchema.py | 221 ++++++++++++++---- tests/orchestrator/test_call_http.py | 4 +- tests/orchestrator/test_create_timer.py | 4 +- tests/orchestrator/test_entity.py | 28 ++- .../test_sequential_orchestrator.py | 2 +- tests/orchestrator/test_sub_orchestrator.py | 4 +- .../test_sub_orchestrator_with_retry.py | 4 +- 9 files changed, 247 insertions(+), 71 deletions(-) create mode 100644 tests/orchestrator/schemas/EntityStateSchema.py diff --git a/tests/orchestrator/orchestrator_test_utils.py b/tests/orchestrator/orchestrator_test_utils.py index b4ca6fcf..d8faee43 100644 --- a/tests/orchestrator/orchestrator_test_utils.py +++ b/tests/orchestrator/orchestrator_test_utils.py @@ -5,6 +5,7 @@ from azure.durable_functions.models import DurableOrchestrationContext, DurableEntityContext from azure.durable_functions.orchestrator import Orchestrator from azure.durable_functions.entity import Entity +from .schemas.EntityStateSchema import schema as entity_state_schema from .schemas.OrchetrationStateSchema import schema @@ -122,6 +123,13 @@ def assert_valid_schema(orchestration_state): assert validation_results is None +def assert_valid_entity_schema(entity_state): + validation_results = validate( + instance=entity_state, + schema=entity_state_schema) + assert validation_results is None + + def assert_dict_are_equal(expected: Dict[Any, Any], result: Dict[Any, Any]): assert len(expected.keys()) == len(result.keys()) for key in expected.keys(): diff --git a/tests/orchestrator/schemas/EntityStateSchema.py b/tests/orchestrator/schemas/EntityStateSchema.py new file mode 100644 index 00000000..017febc5 --- /dev/null +++ b/tests/orchestrator/schemas/EntityStateSchema.py @@ -0,0 +1,43 @@ +schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "entityExists": {"type": "boolean"}, + "entityState": {"type": "string"}, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "isError": {"type": "boolean"}, + "duration": { + "type": "integer", + "minimum": 0 + }, + "startTime": { + "type": "integer", + "minimum": 0 + }, + "result": {"type": "string"} + }, + "required": [ + "isError", + "duration", + "startTime", + "result" + ], + "additionalProperties": False + } + }, + "signals": { + "type": "array" + } + }, + "required": [ + "entityExists", + "entityState", + "results", + "signals" + ], + "additionalProperties": False +} diff --git a/tests/orchestrator/schemas/OrchetrationStateSchema.py b/tests/orchestrator/schemas/OrchetrationStateSchema.py index eb33e44b..836add25 100644 --- a/tests/orchestrator/schemas/OrchetrationStateSchema.py +++ b/tests/orchestrator/schemas/OrchetrationStateSchema.py @@ -1,66 +1,187 @@ +from azure.durable_functions.models.ReplaySchema import ReplaySchema +from azure.durable_functions.models.actions.ActionType import ActionType + + +def _action_schema(action_type, properties, required): + return { + "type": "object", + "properties": { + "actionType": {"const": action_type.value}, + **properties + }, + "required": ["actionType", *required], + "additionalProperties": False + } + + +_retry_options = { + "type": "object", + "properties": { + "firstRetryIntervalInMilliseconds": { + "type": "integer", + "minimum": 1 + }, + "maxNumberOfAttempts": {"type": "integer"} + }, + "required": [ + "firstRetryIntervalInMilliseconds", + "maxNumberOfAttempts" + ], + "additionalProperties": False +} + +_entity_action_properties = { + "instanceId": {"type": "string"}, + "operation": {"type": "string"}, + "input": {"type": "string"} +} + +_sub_orchestrator_properties = { + "functionName": {"type": "string"}, + "input": {"type": "string"}, + "instanceId": {"type": ["string", "null"]}, + "version": {"type": ["string", "null"]} +} + +_http_request = { + "type": "object", + "properties": { + "method": {"type": "string"}, + "uri": {"type": "string"}, + "content": {"type": ["string", "null"]}, + "headers": { + "type": ["object", "null"], + "additionalProperties": {"type": "string"} + }, + "tokenSource": { + "type": "object", + "properties": { + "kind": {"const": "AzureManagedIdentity"}, + "resource": {"type": "string"} + }, + "required": ["kind", "resource"], + "additionalProperties": False + } + }, + "required": ["method", "uri"], + "additionalProperties": False +} + schema = { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "definitions": { + "action": { + "oneOf": [ + _action_schema( + ActionType.CALL_ACTIVITY, + { + "functionName": {"type": "string"}, + "input": {"type": "string"} + }, + ["functionName", "input"] + ), + _action_schema( + ActionType.CALL_ACTIVITY_WITH_RETRY, + { + "functionName": {"type": "string"}, + "input": {"type": "string"}, + "retryOptions": _retry_options + }, + ["functionName", "input", "retryOptions"] + ), + _action_schema( + ActionType.CALL_SUB_ORCHESTRATOR, + _sub_orchestrator_properties, + ["functionName", "input"] + ), + _action_schema( + ActionType.CALL_SUB_ORCHESTRATOR_WITH_RETRY, + { + **_sub_orchestrator_properties, + "retryOptions": _retry_options + }, + ["functionName", "input", "retryOptions"] + ), + _action_schema( + ActionType.CONTINUE_AS_NEW, + {"input": {"type": "string"}}, + ["input"] + ), + _action_schema( + ActionType.CREATE_TIMER, + { + "fireAt": { + "type": "string", + "format": "date-time" + }, + "isCanceled": {"type": "boolean"} + }, + ["fireAt", "isCanceled"] + ), + _action_schema( + ActionType.WAIT_FOR_EXTERNAL_EVENT, + { + "externalEventName": {"type": "string"}, + "reason": {"const": "ExternalEvent"} + }, + ["externalEventName", "reason"] + ), + _action_schema( + ActionType.CALL_ENTITY, + _entity_action_properties, + ["instanceId", "operation", "input"] + ), + _action_schema( + ActionType.CALL_HTTP, + {"httpRequest": _http_request}, + ["httpRequest"] + ), + _action_schema( + ActionType.SIGNAL_ENTITY, + _entity_action_properties, + ["instanceId", "operation", "input"] + ), + _action_schema( + ActionType.WHEN_ANY, + { + "compoundActions": { + "type": "array", + "items": {"$ref": "#/definitions/action"} + } + }, + ["compoundActions"] + ), + _action_schema( + ActionType.WHEN_ALL, + { + "compoundActions": { + "type": "array", + "items": {"$ref": "#/definitions/action"} + } + }, + ["compoundActions"] + ) + ] + } + }, "properties": { "isDone": {"type": "boolean"}, + "schemaVersion": { + "type": "integer", + "enum": [ReplaySchema.V2.value, ReplaySchema.V3.value] + }, "output": {}, "error": {"type": "string"}, - "customStatus": { - "anyOf": [ - { - "type": "object", - },{ - "type": "string", - }] }, + "customStatus": {}, "actions": { "type": "array", "items": { "type": "array", - "items": { - "type": "object", - "properties": { - "functionName": {"type": "string"}, - "actionType": {"type": "number"}, - "input": {}, - "retryOptions": { - "type": "object", - "properties": { - "firstRetryIntervalInMilliseconds": { - "type": "number", - "minimum": 1}, - "maxNumberOfAttempts": {"type": "number"} - }, - "required": - ["firstRetryIntervalInMilliseconds", "maxNumberOfAttempts"], - "additionalProperties": False - }, - "httpRequest": { - "type": "object", - "properties": { - "method": {"type": "string"}, - "uri": {"type": "string"}, - "content": {}, - "headers": {}, - "tokenSource": { - "type": "object", - "properties": { - "resource": {"type": "string"} - }, - "required": ["resource"], - "additionalProperties": False - } - }, - "required": - ["method", "uri"], - "additionalProperties": False - } - }, - "required": ["actionType"], - "additionalProperties": False - } + "items": {"$ref": "#/definitions/action"} } } }, - "required": ["isDone"], + "required": ["isDone", "actions"], "additionalProperties": False } diff --git a/tests/orchestrator/test_call_http.py b/tests/orchestrator/test_call_http.py index b42b36cd..221541f5 100644 --- a/tests/orchestrator/test_call_http.py +++ b/tests/orchestrator/test_call_http.py @@ -138,7 +138,7 @@ def test_initial_post_state(): add_http_action(expected_state, request) expected = expected_state.to_json() - # assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) validate_result_http_request(result) @@ -172,7 +172,7 @@ def test_post_completed_state(): expected_state._is_done = True expected = expected_state.to_json() - # assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) validate_result_http_request(result) diff --git a/tests/orchestrator/test_create_timer.py b/tests/orchestrator/test_create_timer.py index dd15c30e..a8c5483f 100644 --- a/tests/orchestrator/test_create_timer.py +++ b/tests/orchestrator/test_create_timer.py @@ -76,9 +76,7 @@ def test_timers_comparison_with_relaxed_precision(): expected_state._is_done = True expected = expected_state.to_json() - #assert_valid_schema(result) - # TODO: getting the following error when validating the schema - # "Additional properties are not allowed ('fireAt', 'isCanceled' were unexpected)"> + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) def test_timers_can_be_cancelled(): diff --git a/tests/orchestrator/test_entity.py b/tests/orchestrator/test_entity.py index bc405434..33103cc9 100644 --- a/tests/orchestrator/test_entity.py +++ b/tests/orchestrator/test_entity.py @@ -1,7 +1,13 @@ from azure.durable_functions.models.ReplaySchema import ReplaySchema -from .orchestrator_test_utils \ - import assert_orchestration_state_equals, assert_results_are_equal, get_orchestration_state_result, assert_valid_schema, \ - get_entity_state_result, assert_entity_state_equals +from .orchestrator_test_utils import ( + assert_entity_state_equals, + assert_orchestration_state_equals, + assert_results_are_equal, + assert_valid_entity_schema, + assert_valid_schema, + get_entity_state_result, + get_orchestration_state_result +) from tests.test_utils.ContextBuilder import ContextBuilder from tests.test_utils.EntityContextBuilder import EntityContextBuilder from azure.durable_functions.models.OrchestratorState import OrchestratorState @@ -131,7 +137,7 @@ def test_entity_raises_exception(): expected = expected_state.to_json() # Ensure expectation matches observed behavior - #assert_valid_schema(result) + assert_valid_entity_schema(result) assert_entity_state_equals(expected, result) def test_entity_raises_exception_with_pystein(): @@ -153,7 +159,7 @@ def test_entity_raises_exception_with_pystein(): expected = expected_state.to_json() # Ensure expectation matches observed behavior - #assert_valid_schema(result) + assert_valid_entity_schema(result) assert_entity_state_equals(expected, result) def test_entity_signal_then_call(): @@ -179,7 +185,7 @@ def test_entity_signal_then_call(): expected = expected_state.to_json() # Ensure expectation matches observed behavior - #assert_valid_schema(result) + assert_valid_entity_schema(result) assert_entity_state_equals(expected, result) def test_entity_set_then_get_with_preexisting_raw_state(): @@ -263,7 +269,7 @@ def test_entity_signal_then_call_with_pystein(): expected = expected_state.to_json() # Ensure expectation matches observed behavior - #assert_valid_schema(result) + assert_valid_entity_schema(result) assert_entity_state_equals(expected, result) def apply_operation(entity_state: EntityState, result: Any, state: Any, is_error: bool = False): @@ -365,7 +371,7 @@ def test_call_entity_sent(): add_call_entity_action(expected_state, entityId, "add", 3) expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) def test_signal_entity_sent(): @@ -380,7 +386,7 @@ def test_signal_entity_sent(): add_call_entity_action(expected_state, entityId, "get", None) expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) def test_signal_entity_sent_and_response_received(): @@ -398,7 +404,7 @@ def test_signal_entity_sent_and_response_received(): expected_state._is_done = True expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) @@ -418,7 +424,7 @@ def test_call_entity_raised(): expected_state._is_done = True expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) diff --git a/tests/orchestrator/test_sequential_orchestrator.py b/tests/orchestrator/test_sequential_orchestrator.py index 56c533d4..05e0f3cb 100644 --- a/tests/orchestrator/test_sequential_orchestrator.py +++ b/tests/orchestrator/test_sequential_orchestrator.py @@ -837,5 +837,5 @@ def test_compound_tasks_return_single_action_in_V2(): expected_state._is_done = False expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) diff --git a/tests/orchestrator/test_sub_orchestrator.py b/tests/orchestrator/test_sub_orchestrator.py index bc719678..4775cfbc 100644 --- a/tests/orchestrator/test_sub_orchestrator.py +++ b/tests/orchestrator/test_sub_orchestrator.py @@ -71,7 +71,7 @@ def test_tokyo_and_seattle_and_london_state(): expected_state._is_done = True expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) def test_call_suborchestrator_by_name(): @@ -91,5 +91,5 @@ def test_call_suborchestrator_by_name(): expected_state._is_done = True expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) diff --git a/tests/orchestrator/test_sub_orchestrator_with_retry.py b/tests/orchestrator/test_sub_orchestrator_with_retry.py index ae8db018..4ca781f5 100644 --- a/tests/orchestrator/test_sub_orchestrator_with_retry.py +++ b/tests/orchestrator/test_sub_orchestrator_with_retry.py @@ -71,7 +71,7 @@ def test_tokyo_and_seattle_and_london_state(): expected_state._is_done = True expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) @@ -96,7 +96,7 @@ def test_tokyo_and_seattle_and_london_state_partial_failure(): expected_state._is_done = True expected = expected_state.to_json() - #assert_valid_schema(result) + assert_valid_schema(result) assert_orchestration_state_equals(expected, result) def test_tokyo_and_seattle_and_london_state_all_failed():