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 time import mktime
|
||||
|
||||
from ..utils import decapitalize
|
||||
|
||||
|
||||
class HistoryEvent(object):
|
||||
def __init__(self, event_id, event_type, **kwargs):
|
||||
@ -23,8 +25,7 @@ class HistoryEvent(object):
|
||||
|
||||
def _attributes_key(self):
|
||||
key = "{}EventAttributes".format(self.event_type)
|
||||
key = key[0].lower() + key[1:]
|
||||
return key
|
||||
return decapitalize(key)
|
||||
|
||||
def event_attributes(self):
|
||||
if self.event_type == "WorkflowExecutionStarted":
|
||||
|
@ -3,11 +3,15 @@ import uuid
|
||||
|
||||
from moto.core.utils import camelcase_to_underscores
|
||||
|
||||
from ..constants import (
|
||||
DECISIONS_FIELDS,
|
||||
)
|
||||
from ..exceptions import (
|
||||
SWFDefaultUndefinedFault,
|
||||
SWFValidationException,
|
||||
SWFDecisionValidationException,
|
||||
)
|
||||
from ..utils import decapitalize
|
||||
from .decision_task import DecisionTask
|
||||
from .history_event import HistoryEvent
|
||||
|
||||
@ -189,6 +193,19 @@ class WorkflowExecution(object):
|
||||
dt.complete()
|
||||
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):
|
||||
"""
|
||||
Performs some basic validations on decisions. The real SWF service
|
||||
@ -202,7 +219,7 @@ class WorkflowExecution(object):
|
||||
problems = []
|
||||
|
||||
# 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]:
|
||||
close_decision_types = [
|
||||
"CompleteWorkflowExecution",
|
||||
@ -217,7 +234,16 @@ class WorkflowExecution(object):
|
||||
decision_number = 0
|
||||
for dcs in decisions:
|
||||
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
|
||||
if dcs["decisionType"] not in self.KNOWN_DECISION_TYPES:
|
||||
problems.append({
|
||||
@ -243,9 +269,7 @@ class WorkflowExecution(object):
|
||||
# handle each decision separately, in order
|
||||
for decision in decisions:
|
||||
decision_type = decision["decisionType"]
|
||||
attributes_key = "{}{}EventAttributes".format(
|
||||
decision_type[0].lower(), decision_type[1:]
|
||||
)
|
||||
attributes_key = "{}EventAttributes".format(decapitalize(decision_type))
|
||||
attributes = decision.get(attributes_key, {})
|
||||
if decision_type == "CompleteWorkflowExecution":
|
||||
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,
|
||||
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…
Reference in New Issue
Block a user