moto/tests/test_swf/responses/test_decision_tasks.py
2024-01-27 19:38:09 +00:00

646 lines
20 KiB
Python

import re
from datetime import datetime
from time import sleep
import pytest
from botocore.exceptions import ClientError
from dateutil.parser import parse as dtparse
from freezegun import freeze_time
from moto import mock_aws, settings
from ..utils import setup_workflow_boto3
# PollForDecisionTask endpoint
@mock_aws
def test_poll_for_decision_task_when_one_boto3():
client = setup_workflow_boto3()
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == ["WorkflowExecutionStarted", "DecisionTaskScheduled"]
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}, identity="srv01"
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
]
assert (
resp["events"][-1]["decisionTaskStartedEventAttributes"]["identity"] == "srv01"
)
@mock_aws
def test_poll_for_decision_task_previous_started_event_id_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
assert resp["workflowExecution"]["runId"] == client.run_id
assert "previousStartedEventId" not in resp
# Require a failing decision, in this case a non-existant activity type
attrs = {
"activityId": "spam",
"activityType": {"name": "test-activity", "version": "v1.42"},
"taskList": {"name": "eggs"},
}
decision = {
"decisionType": "ScheduleActivityTask",
"scheduleActivityTaskDecisionAttributes": attrs,
}
client.respond_decision_task_completed(
taskToken=resp["taskToken"], decisions=[decision]
)
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
assert resp["workflowExecution"]["runId"] == client.run_id
assert resp["previousStartedEventId"] == 3
@mock_aws
def test_poll_for_decision_task_ensure_single_started_task():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
assert "taskToken" in resp
first_decision_task = resp["taskToken"]
# History should have just the decision task triggered on workflow start
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
]
# Schedule another decision task, before first one is completed
client.signal_workflow_execution(
domain="test-domain",
signalName="my_signal",
workflowId="uid-abcd1234",
input="my_input",
runId=client.run_id,
)
# Second poll should return no new tasks
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
assert resp["previousStartedEventId"] == 0
assert resp["startedEventId"] == 0
assert "taskToken" not in resp
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"WorkflowExecutionSignaled",
"DecisionTaskScheduled",
]
client.respond_decision_task_completed(taskToken=first_decision_task)
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
assert "taskToken" in resp
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"WorkflowExecutionSignaled",
"DecisionTaskScheduled",
"DecisionTaskCompleted",
"DecisionTaskStarted",
]
@mock_aws
def test_poll_for_decision_task_exclude_completed_executions():
client = setup_workflow_boto3()
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == ["WorkflowExecutionStarted", "DecisionTaskScheduled"]
client.terminate_workflow_execution(
domain="test-domain", runId=client.run_id, workflowId="uid-abcd1234"
)
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
assert "taskToken" not in resp
@mock_aws
def test_poll_for_decision_task_when_none_boto3():
client = setup_workflow_boto3()
client.poll_for_decision_task(domain="test-domain", taskList={"name": "queue"})
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
# this is the DecisionTask representation you get from the real SWF
# after waiting 60s when there's no decision to be taken
assert resp["previousStartedEventId"] == 0
assert resp["startedEventId"] == 0
@mock_aws
def test_poll_for_decision_task_on_non_existent_queue_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "non-existent-queue"}
)
assert resp["previousStartedEventId"] == 0
assert resp["startedEventId"] == 0
@mock_aws
def test_poll_for_decision_task_with_reverse_order_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}, reverseOrder=True
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"DecisionTaskStarted",
"DecisionTaskScheduled",
"WorkflowExecutionStarted",
]
# CountPendingDecisionTasks endpoint
@mock_aws
def test_count_pending_decision_tasks_boto3():
client = setup_workflow_boto3()
client.poll_for_decision_task(domain="test-domain", taskList={"name": "queue"})
resp = client.count_pending_decision_tasks(
domain="test-domain", taskList={"name": "queue"}
)
assert resp["count"] == 1
assert resp["truncated"] is False
@mock_aws
def test_count_pending_decision_tasks_on_non_existent_task_list_boto3():
client = setup_workflow_boto3()
resp = client.count_pending_decision_tasks(
domain="test-domain", taskList={"name": "non-existent"}
)
assert resp["count"] == 0
assert resp["truncated"] is False
@mock_aws
def test_count_pending_decision_tasks_after_decision_completes_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
client.respond_decision_task_completed(taskToken=resp["taskToken"])
resp = client.count_pending_decision_tasks(
domain="test-domain", taskList={"name": "queue"}
)
assert resp["count"] == 0
assert resp["truncated"] is False
# RespondDecisionTaskCompleted endpoint
@mock_aws
def test_respond_decision_task_completed_with_no_decision_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
client.respond_decision_task_completed(
taskToken=task_token, executionContext="free-form context"
)
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"DecisionTaskCompleted",
]
evt = resp["events"][-1]
assert evt["decisionTaskCompletedEventAttributes"] == {
"executionContext": "free-form context",
"scheduledEventId": 2,
"startedEventId": 3,
}
resp = client.describe_workflow_execution(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
assert resp["latestExecutionContext"] == "free-form context"
@mock_aws
def test_respond_decision_task_completed_with_wrong_token_boto3():
client = setup_workflow_boto3()
client.poll_for_decision_task(domain="test-domain", taskList={"name": "queue"})
with pytest.raises(ClientError) as ex:
client.respond_decision_task_completed(taskToken="not-a-correct-token")
assert ex.value.response["Error"]["Code"] == "ValidationException"
assert ex.value.response["Error"]["Message"] == "Invalid token"
assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
@mock_aws
def test_respond_decision_task_completed_on_close_workflow_execution_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
client.terminate_workflow_execution(domain="test-domain", workflowId="uid-abcd1234")
with pytest.raises(ClientError) as ex:
client.respond_decision_task_completed(taskToken=task_token)
assert ex.value.response["Error"]["Code"] == "UnknownResourceFault"
assert ex.value.response["Error"]["Message"] == (
f"Unknown execution: WorkflowExecution=[workflowId=uid-abcd1234, runId={client.run_id}]"
)
assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
@mock_aws
def test_respond_decision_task_completed_with_task_already_completed_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
client.respond_decision_task_completed(taskToken=task_token)
with pytest.raises(ClientError) as ex:
client.respond_decision_task_completed(taskToken=task_token)
assert ex.value.response["Error"]["Code"] == "UnknownResourceFault"
assert ex.value.response["Error"]["Message"] == (
"Unknown decision task, scheduledEventId = 2"
)
assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
@mock_aws
def test_respond_decision_task_completed_with_complete_workflow_execution_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [
{
"decisionType": "CompleteWorkflowExecution",
"completeWorkflowExecutionDecisionAttributes": {"result": "foo bar"},
}
]
client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"DecisionTaskCompleted",
"WorkflowExecutionCompleted",
]
assert (
resp["events"][-1]["workflowExecutionCompletedEventAttributes"]["result"]
== "foo bar"
)
@mock_aws
def test_respond_decision_task_completed_with_close_decision_not_last_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [
{"decisionType": "CompleteWorkflowExecution"},
{"decisionType": "WeDontCare"},
]
with pytest.raises(ClientError) as ex:
client.respond_decision_task_completed(
taskToken=task_token, decisions=decisions
)
assert ex.value.response["Error"]["Code"] == "ValidationException"
assert ex.value.response["Error"]["Message"] == (
"Close must be last decision in list"
)
assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
@mock_aws
def test_respond_decision_task_completed_with_invalid_decision_type_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [
{"decisionType": "BadDecisionType"},
{"decisionType": "CompleteWorkflowExecution"},
]
with pytest.raises(ClientError) as ex:
client.respond_decision_task_completed(
taskToken=task_token, decisions=decisions
)
assert ex.value.response["Error"]["Code"] == "ValidationException"
assert re.search(
"Value 'BadDecisionType' at 'decisions.1.member.decisionType'",
ex.value.response["Error"]["Message"],
)
assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
@mock_aws
def test_respond_decision_task_completed_with_missing_attributes_totally_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [{"decisionType": "StartTimer"}]
with pytest.raises(ClientError) as ex:
client.respond_decision_task_completed(
taskToken=task_token, decisions=decisions
)
assert ex.value.response["Error"]["Code"] == "ValidationException"
assert re.search(
(
"Value null at "
"'decisions.1.member.startTimerDecisionAttributes.timerId' "
"failed to satisfy constraint"
),
ex.value.response["Error"]["Message"],
)
assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
@mock_aws
def test_respond_decision_task_completed_with_fail_workflow_execution_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [
{
"decisionType": "FailWorkflowExecution",
"failWorkflowExecutionDecisionAttributes": {
"reason": "my rules",
"details": "foo",
},
}
]
client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"DecisionTaskCompleted",
"WorkflowExecutionFailed",
]
attrs = resp["events"][-1]["workflowExecutionFailedEventAttributes"]
assert attrs["reason"] == "my rules"
assert attrs["details"] == "foo"
@mock_aws
@freeze_time("2015-01-01 12:00:00 UTC")
def test_respond_decision_task_completed_with_schedule_activity_task_boto3():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [
{
"decisionType": "ScheduleActivityTask",
"scheduleActivityTaskDecisionAttributes": {
"activityId": "my-activity-001",
"activityType": {"name": "test-activity", "version": "v1.1"},
"heartbeatTimeout": "60",
"input": "123",
"taskList": {"name": "my-task-list"},
},
}
]
client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"DecisionTaskCompleted",
"ActivityTaskScheduled",
]
assert resp["events"][-1]["activityTaskScheduledEventAttributes"] == {
"decisionTaskCompletedEventId": 4,
"activityId": "my-activity-001",
"activityType": {"name": "test-activity", "version": "v1.1"},
"heartbeatTimeout": "60",
"input": "123",
"taskList": {"name": "my-task-list"},
}
resp = client.describe_workflow_execution(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
assert isinstance(resp["latestActivityTaskTimestamp"], datetime)
if not settings.TEST_SERVER_MODE:
ts = resp["latestActivityTaskTimestamp"]
assert ts == dtparse("2015-01-01 12:00:00 UTC")
@mock_aws
def test_record_marker_decision():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [
{
"decisionType": "RecordMarker",
"recordMarkerDecisionAttributes": {"markerName": "TheMarker"},
}
]
client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"DecisionTaskCompleted",
"MarkerRecorded",
]
assert resp["events"][-1]["markerRecordedEventAttributes"] == {
"decisionTaskCompletedEventId": 4,
"markerName": "TheMarker",
}
@mock_aws
def test_start_and_fire_timer_decision():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [
{
"decisionType": "StartTimer",
"startTimerDecisionAttributes": {
"startToFireTimeout": "1",
"timerId": "timer1",
},
}
]
client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)
sleep(1.1)
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"DecisionTaskCompleted",
"TimerStarted",
"TimerFired",
"DecisionTaskScheduled",
]
assert resp["events"][-3]["timerStartedEventAttributes"] == {
"decisionTaskCompletedEventId": 4,
"startToFireTimeout": "1",
"timerId": "timer1",
}
assert resp["events"][-2]["timerFiredEventAttributes"] == {
"startedEventId": 5,
"timerId": "timer1",
}
@mock_aws
def test_cancel_workflow_decision():
client = setup_workflow_boto3()
resp = client.poll_for_decision_task(
domain="test-domain", taskList={"name": "queue"}
)
task_token = resp["taskToken"]
decisions = [
{
"decisionType": "CancelWorkflowExecution",
"cancelWorkflowExecutionDecisionAttributes": {
"details": "decide to cancel"
},
}
]
client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)
resp = client.get_workflow_execution_history(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)
types = [evt["eventType"] for evt in resp["events"]]
assert types == [
"WorkflowExecutionStarted",
"DecisionTaskScheduled",
"DecisionTaskStarted",
"DecisionTaskCompleted",
"WorkflowExecutionCanceled",
]
assert resp["events"][-1]["workflowExecutionCanceledEventAttributes"] == {
"decisionTaskCompletedEventId": 4,
"details": "decide to cancel",
}
workflow_result = client.describe_workflow_execution(
domain="test-domain",
execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
)["executionInfo"]
assert "closeTimestamp" in workflow_result
assert workflow_result["executionStatus"] == "CLOSED"
assert workflow_result["closeStatus"] == "CANCELED"
assert workflow_result["cancelRequested"] is True