Add checks for *DecisionAttributes within RespondDecisionTaskCompleted

This commit is contained in:
Jean-Baptiste Barth 2015-10-19 09:25:54 +02:00
parent 507351612e
commit 558b84fb6a
6 changed files with 169 additions and 7 deletions

85
moto/swf/constants.py Normal file
View 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 }
}
}

View File

@ -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":

View File

@ -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
View File

@ -0,0 +1,2 @@
def decapitalize(key):
return key[0].lower() + key[1:]

View File

@ -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"
)

View 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)