Add checks for *DecisionAttributes within RespondDecisionTaskCompleted
This commit is contained in:
parent
507351612e
commit
558b84fb6a
85
moto/swf/constants.py
Normal file
85
moto/swf/constants.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# List decision fields and if they're required or not
|
||||||
|
#
|
||||||
|
# See http://docs.aws.amazon.com/amazonswf/latest/apireference/API_RespondDecisionTaskCompleted.html
|
||||||
|
# and subsequent docs for each decision type.
|
||||||
|
DECISIONS_FIELDS = {
|
||||||
|
"cancelTimerDecisionAttributes": {
|
||||||
|
"timerId": { "type": "string", "required": True }
|
||||||
|
},
|
||||||
|
"cancelWorkflowExecutionDecisionAttributes": {
|
||||||
|
"details": { "type": "string", "required": False }
|
||||||
|
},
|
||||||
|
"completeWorkflowExecutionDecisionAttributes": {
|
||||||
|
"result": { "type": "string", "required": False }
|
||||||
|
},
|
||||||
|
"continueAsNewWorkflowExecutionDecisionAttributes": {
|
||||||
|
"childPolicy": { "type": "string", "required": False },
|
||||||
|
"executionStartToCloseTimeout": { "type": "string", "required": False },
|
||||||
|
"input": { "type": "string", "required": False },
|
||||||
|
"lambdaRole": { "type": "string", "required": False },
|
||||||
|
"tagList": { "type": "string", "array": True, "required": False },
|
||||||
|
"taskList": { "type": "TaskList", "required": False },
|
||||||
|
"taskPriority": { "type": "string", "required": False },
|
||||||
|
"taskStartToCloseTimeout": { "type": "string", "required": False },
|
||||||
|
"workflowTypeVersion": { "type": "string", "required": False }
|
||||||
|
},
|
||||||
|
"failWorkflowExecutionDecisionAttributes": {
|
||||||
|
"details": { "type": "string", "required": False },
|
||||||
|
"reason": { "type": "string", "required": False }
|
||||||
|
},
|
||||||
|
"recordMarkerDecisionAttributes": {
|
||||||
|
"details": { "type": "string", "required": False },
|
||||||
|
"markerName": { "type": "string", "required": True }
|
||||||
|
},
|
||||||
|
"requestCancelActivityTaskDecisionAttributes": {
|
||||||
|
"activityId": { "type": "string", "required": True }
|
||||||
|
},
|
||||||
|
"requestCancelExternalWorkflowExecutionDecisionAttributes": {
|
||||||
|
"control": { "type": "string", "required": False },
|
||||||
|
"runId": { "type": "string", "required": False },
|
||||||
|
"workflowId": { "type": "string", "required": True }
|
||||||
|
},
|
||||||
|
"scheduleActivityTaskDecisionAttributes": {
|
||||||
|
"activityId": { "type": "string", "required": True },
|
||||||
|
"activityType": { "type": "ActivityType", "required": True },
|
||||||
|
"control": { "type": "string", "required": False },
|
||||||
|
"heartbeatTimeout": { "type": "string", "required": False },
|
||||||
|
"input": { "type": "string", "required": False },
|
||||||
|
"scheduleToCloseTimeout": { "type": "string", "required": False },
|
||||||
|
"scheduleToStartTimeout": { "type": "string", "required": False },
|
||||||
|
"startToCloseTimeout": { "type": "string", "required": False },
|
||||||
|
"taskList": { "type": "TaskList", "required": False },
|
||||||
|
"taskPriority": { "type": "string", "required": False }
|
||||||
|
},
|
||||||
|
"scheduleLambdaFunctionDecisionAttributes": {
|
||||||
|
"id": { "type": "string", "required": True },
|
||||||
|
"input": { "type": "string", "required": False },
|
||||||
|
"name": { "type": "string", "required": True },
|
||||||
|
"startToCloseTimeout": { "type": "string", "required": False }
|
||||||
|
},
|
||||||
|
"signalExternalWorkflowExecutionDecisionAttributes": {
|
||||||
|
"control": { "type": "string", "required": False },
|
||||||
|
"input": { "type": "string", "required": False },
|
||||||
|
"runId": { "type": "string", "required": False },
|
||||||
|
"signalName": { "type": "string", "required": True },
|
||||||
|
"workflowId": { "type": "string", "required": True }
|
||||||
|
},
|
||||||
|
"startChildWorkflowExecutionDecisionAttributes": {
|
||||||
|
"childPolicy": { "type": "string", "required": False },
|
||||||
|
"control": { "type": "string", "required": False },
|
||||||
|
"executionStartToCloseTimeout": { "type": "string", "required": False },
|
||||||
|
"input": { "type": "string", "required": False },
|
||||||
|
"lambdaRole": { "type": "string", "required": False },
|
||||||
|
"tagList": { "type": "string", "array": True, "required": False },
|
||||||
|
"taskList": { "type": "TaskList", "required": False },
|
||||||
|
"taskPriority": { "type": "string", "required": False },
|
||||||
|
"taskStartToCloseTimeout": { "type": "string", "required": False },
|
||||||
|
"workflowId": { "type": "string", "required": True },
|
||||||
|
"workflowType": { "type": "WorkflowType", "required": True }
|
||||||
|
},
|
||||||
|
"startTimerDecisionAttributes": {
|
||||||
|
"control": { "type": "string", "required": False },
|
||||||
|
"startToFireTimeout": { "type": "string", "required": True },
|
||||||
|
"timerId": { "type": "string", "required": True }
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@ from __future__ import unicode_literals
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from time import mktime
|
from time import mktime
|
||||||
|
|
||||||
|
from ..utils import decapitalize
|
||||||
|
|
||||||
|
|
||||||
class HistoryEvent(object):
|
class HistoryEvent(object):
|
||||||
def __init__(self, event_id, event_type, **kwargs):
|
def __init__(self, event_id, event_type, **kwargs):
|
||||||
@ -23,8 +25,7 @@ class HistoryEvent(object):
|
|||||||
|
|
||||||
def _attributes_key(self):
|
def _attributes_key(self):
|
||||||
key = "{}EventAttributes".format(self.event_type)
|
key = "{}EventAttributes".format(self.event_type)
|
||||||
key = key[0].lower() + key[1:]
|
return decapitalize(key)
|
||||||
return key
|
|
||||||
|
|
||||||
def event_attributes(self):
|
def event_attributes(self):
|
||||||
if self.event_type == "WorkflowExecutionStarted":
|
if self.event_type == "WorkflowExecutionStarted":
|
||||||
|
@ -3,11 +3,15 @@ import uuid
|
|||||||
|
|
||||||
from moto.core.utils import camelcase_to_underscores
|
from moto.core.utils import camelcase_to_underscores
|
||||||
|
|
||||||
|
from ..constants import (
|
||||||
|
DECISIONS_FIELDS,
|
||||||
|
)
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
SWFDefaultUndefinedFault,
|
SWFDefaultUndefinedFault,
|
||||||
SWFValidationException,
|
SWFValidationException,
|
||||||
SWFDecisionValidationException,
|
SWFDecisionValidationException,
|
||||||
)
|
)
|
||||||
|
from ..utils import decapitalize
|
||||||
from .decision_task import DecisionTask
|
from .decision_task import DecisionTask
|
||||||
from .history_event import HistoryEvent
|
from .history_event import HistoryEvent
|
||||||
|
|
||||||
@ -189,6 +193,19 @@ class WorkflowExecution(object):
|
|||||||
dt.complete()
|
dt.complete()
|
||||||
self.handle_decisions(evt.event_id, decisions)
|
self.handle_decisions(evt.event_id, decisions)
|
||||||
|
|
||||||
|
def _check_decision_attributes(self, kind, value, decision_id):
|
||||||
|
problems = []
|
||||||
|
constraints = DECISIONS_FIELDS.get(kind, {})
|
||||||
|
for key, constraint in constraints.iteritems():
|
||||||
|
if constraint["required"] and not value.get(key):
|
||||||
|
problems.append({
|
||||||
|
"type": "null_value",
|
||||||
|
"where": "decisions.{}.member.{}.{}".format(
|
||||||
|
decision_id, kind, key
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return problems
|
||||||
|
|
||||||
def validate_decisions(self, decisions):
|
def validate_decisions(self, decisions):
|
||||||
"""
|
"""
|
||||||
Performs some basic validations on decisions. The real SWF service
|
Performs some basic validations on decisions. The real SWF service
|
||||||
@ -202,7 +219,7 @@ class WorkflowExecution(object):
|
|||||||
problems = []
|
problems = []
|
||||||
|
|
||||||
# check close decision is last
|
# check close decision is last
|
||||||
# TODO: see what happens on real SWF service if we ask for 2 close decisions
|
# (the real SWF service also works that way if you provide 2 close decision tasks)
|
||||||
for dcs in decisions[:-1]:
|
for dcs in decisions[:-1]:
|
||||||
close_decision_types = [
|
close_decision_types = [
|
||||||
"CompleteWorkflowExecution",
|
"CompleteWorkflowExecution",
|
||||||
@ -217,7 +234,16 @@ class WorkflowExecution(object):
|
|||||||
decision_number = 0
|
decision_number = 0
|
||||||
for dcs in decisions:
|
for dcs in decisions:
|
||||||
decision_number += 1
|
decision_number += 1
|
||||||
# TODO: check decision types mandatory attributes
|
# check decision types mandatory attributes
|
||||||
|
# NB: the real SWF service seems to check attributes even for attributes list
|
||||||
|
# that are not in line with the decisionType, so we do the same
|
||||||
|
attrs_to_check = filter(lambda x: x.endswith("DecisionAttributes"), dcs.keys())
|
||||||
|
if dcs["decisionType"] in self.KNOWN_DECISION_TYPES:
|
||||||
|
decision_type = dcs["decisionType"]
|
||||||
|
decision_attr = "{}DecisionAttributes".format(decapitalize(decision_type))
|
||||||
|
attrs_to_check.append(decision_attr)
|
||||||
|
for attr in attrs_to_check:
|
||||||
|
problems += self._check_decision_attributes(attr, dcs.get(attr, {}), decision_number)
|
||||||
# check decision type is correct
|
# check decision type is correct
|
||||||
if dcs["decisionType"] not in self.KNOWN_DECISION_TYPES:
|
if dcs["decisionType"] not in self.KNOWN_DECISION_TYPES:
|
||||||
problems.append({
|
problems.append({
|
||||||
@ -243,9 +269,7 @@ class WorkflowExecution(object):
|
|||||||
# handle each decision separately, in order
|
# handle each decision separately, in order
|
||||||
for decision in decisions:
|
for decision in decisions:
|
||||||
decision_type = decision["decisionType"]
|
decision_type = decision["decisionType"]
|
||||||
attributes_key = "{}{}EventAttributes".format(
|
attributes_key = "{}EventAttributes".format(decapitalize(decision_type))
|
||||||
decision_type[0].lower(), decision_type[1:]
|
|
||||||
)
|
|
||||||
attributes = decision.get(attributes_key, {})
|
attributes = decision.get(attributes_key, {})
|
||||||
if decision_type == "CompleteWorkflowExecution":
|
if decision_type == "CompleteWorkflowExecution":
|
||||||
self.complete(event_id, attributes.get("result"))
|
self.complete(event_id, attributes.get("result"))
|
||||||
|
2
moto/swf/utils.py
Normal file
2
moto/swf/utils.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
def decapitalize(key):
|
||||||
|
return key[0].lower() + key[1:]
|
@ -191,3 +191,42 @@ def test_respond_decision_task_completed_with_invalid_decision_type():
|
|||||||
SWFDecisionValidationException,
|
SWFDecisionValidationException,
|
||||||
r"Value 'BadDecisionType' at 'decisions.1.member.decisionType'"
|
r"Value 'BadDecisionType' at 'decisions.1.member.decisionType'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@mock_swf
|
||||||
|
def test_respond_decision_task_completed_with_missing_attributes():
|
||||||
|
conn = setup_workflow()
|
||||||
|
resp = conn.poll_for_decision_task("test-domain", "queue")
|
||||||
|
task_token = resp["taskToken"]
|
||||||
|
|
||||||
|
decisions = [
|
||||||
|
{
|
||||||
|
"decisionType": "should trigger even with incorrect decision type",
|
||||||
|
"startTimerDecisionAttributes": {}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
conn.respond_decision_task_completed.when.called_with(
|
||||||
|
task_token, decisions=decisions
|
||||||
|
).should.throw(
|
||||||
|
SWFDecisionValidationException,
|
||||||
|
r"Value null at 'decisions.1.member.startTimerDecisionAttributes.timerId' " \
|
||||||
|
r"failed to satisfy constraint: Member must not be null"
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock_swf
|
||||||
|
def test_respond_decision_task_completed_with_missing_attributes_totally():
|
||||||
|
conn = setup_workflow()
|
||||||
|
resp = conn.poll_for_decision_task("test-domain", "queue")
|
||||||
|
task_token = resp["taskToken"]
|
||||||
|
|
||||||
|
decisions = [
|
||||||
|
{ "decisionType": "StartTimer" },
|
||||||
|
]
|
||||||
|
|
||||||
|
conn.respond_decision_task_completed.when.called_with(
|
||||||
|
task_token, decisions=decisions
|
||||||
|
).should.throw(
|
||||||
|
SWFDecisionValidationException,
|
||||||
|
r"Value null at 'decisions.1.member.startTimerDecisionAttributes.timerId' " \
|
||||||
|
r"failed to satisfy constraint: Member must not be null"
|
||||||
|
)
|
||||||
|
11
tests/test_swf/test_utils.py
Normal file
11
tests/test_swf/test_utils.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from sure import expect
|
||||||
|
from moto.swf.utils import decapitalize
|
||||||
|
|
||||||
|
def test_decapitalize():
|
||||||
|
cases = {
|
||||||
|
"fooBar": "fooBar",
|
||||||
|
"FooBar": "fooBar",
|
||||||
|
"FOO BAR": "fOO BAR",
|
||||||
|
}
|
||||||
|
for before, after in cases.iteritems():
|
||||||
|
decapitalize(before).should.equal(after)
|
Loading…
x
Reference in New Issue
Block a user