def test_composite_handler_stops_handler_chain(self): def inner1(_chain: HandlerChain, request: RequestContext, response: Response): _chain.stop() inner2 = mock.MagicMock() outer1 = mock.MagicMock() outer2 = mock.MagicMock() response1 = mock.MagicMock() chain = HandlerChain() composite = CompositeHandler() composite.handlers.append(inner1) composite.handlers.append(inner2) chain.request_handlers.append(outer1) chain.request_handlers.append(composite) chain.request_handlers.append(outer2) chain.response_handlers.append(response1) chain.handle(RequestContext(), Response()) outer1.assert_called_once() outer2.assert_not_called() inner2.assert_not_called() response1.assert_called_once()
def test_composite_handler_exception_calls_outer_exception_handlers(self): def inner1(_chain: HandlerChain, request: RequestContext, response: Response): raise ValueError() inner2 = mock.MagicMock() outer1 = mock.MagicMock() outer2 = mock.MagicMock() exception_handler = mock.MagicMock() response1 = mock.MagicMock() chain = HandlerChain() composite = CompositeHandler() composite.handlers.append(inner1) composite.handlers.append(inner2) chain.request_handlers.append(outer1) chain.request_handlers.append(composite) chain.request_handlers.append(outer2) chain.exception_handlers.append(exception_handler) chain.response_handlers.append(response1) chain.handle(RequestContext(), Response()) outer1.assert_called_once() outer2.assert_not_called() inner2.assert_not_called() exception_handler.assert_called_once() response1.assert_called_once()
def test_without_context_without_expand(self): def fn(*args): assert len(args) == 1 assert type(args[0]) == dict dispatcher = ServiceRequestDispatcher(fn, "SomeAction", pass_context=False, expand_parameters=False) dispatcher(RequestContext(), ServiceRequest())
def test_default_dispatcher(self): class SomeAction(ServiceRequest): ArgOne: str ArgTwo: int def fn(context, arg_one, arg_two): assert type(context) == RequestContext assert arg_one == "foo" assert arg_two == 69 dispatcher = ServiceRequestDispatcher(fn, "SomeAction") dispatcher(RequestContext(), SomeAction(ArgOne="foo", ArgTwo=69))
def create_request_context(self, request: HttpRequest) -> RequestContext: context = RequestContext() context.service = self.service context.request = request context.region = get_region(request) context.account_id = get_account_id(request) return context
def test_dispatch_missing_method_returns_internal_failure(): table: DispatchTable = dict() sqs_service = load_service("sqs") skeleton = Skeleton(sqs_service, table) context = RequestContext() context.account = "test" context.region = "us-west-1" context.service = sqs_service context.request = { "method": "POST", "path": "/", "body": "Action=DeleteQueue&Version=2012-11-05&QueueUrl=http%3A%2F%2Flocalhost%3A4566%2F000000000000%2Ftf-acc-test-queue", "headers": _get_sqs_request_headers(), } result = skeleton.invoke(context) # Use the parser from botocore to parse the serialized response response_parser = create_parser(sqs_service.protocol) parsed_response = response_parser.parse( result, sqs_service.operation_model("SendMessage").output_shape) assert "Error" in parsed_response assert parsed_response["Error"] == { "Code": "InternalFailure", "Message": "API action 'DeleteQueue' for service 'sqs' not yet implemented", }
def test_dispatch_common_service_exception(): def delete_queue(_context: RequestContext, _request: ServiceRequest): raise CommonServiceException("NonExistentQueue", "No such queue") table: DispatchTable = dict() table["DeleteQueue"] = delete_queue sqs_service = load_service("sqs") skeleton = Skeleton(sqs_service, table) context = RequestContext() context.account = "test" context.region = "us-west-1" context.service = sqs_service context.request = { "method": "POST", "path": "/", "body": "Action=DeleteQueue&Version=2012-11-05&QueueUrl=http%3A%2F%2Flocalhost%3A4566%2F000000000000%2Ftf-acc-test-queue", "headers": _get_sqs_request_headers(), } result = skeleton.invoke(context) # Use the parser from botocore to parse the serialized response response_parser = create_parser(sqs_service.protocol) parsed_response = response_parser.parse( result, sqs_service.operation_model("SendMessage").output_shape) assert "Error" in parsed_response assert parsed_response["Error"] == { "Code": "NonExistentQueue", "Message": "No such queue", }
def invoke(self, context: RequestContext) -> HttpResponse: if context.operation and context.service_request: # if the parsed request is already set in the context, re-use them operation, instance = context.operation, context.service_request else: # otherwise parse the incoming HTTPRequest operation, instance = self.parser.parse(context.request) context.operation = operation serializer = self.serializer try: # Find the operation's handler in the dispatch table if operation.name not in self.dispatch_table: LOG.warning( "missing entry in dispatch table for %s.%s", self.service.service_name, operation.name, ) raise NotImplementedError handler = self.dispatch_table[operation.name] # Call the appropriate handler result = handler(context, instance) or dict() # Serialize result dict to an HTTPResponse and return it return serializer.serialize_to_response(result, operation) except ServiceException as e: return serializer.serialize_error_to_response(e, operation) except NotImplementedError: action_name = operation.name service_name = operation.service_model.service_name message = ( f"API action '{action_name}' for service '{service_name}' " f"not yet implemented") LOG.info(message) error = CommonServiceException("InternalFailure", message, status_code=501) # record event analytics.log.event("services_notimplemented", payload={ "s": service_name, "a": action_name }) return serializer.serialize_error_to_response(error, operation)
def create_aws_request_context( service_name: str, action: str, parameters: Mapping[str, Any] = None, region: str = None, endpoint_url: Optional[str] = None, ) -> RequestContext: """ This is a stripped-down version of what the botocore client does to perform an HTTP request from a client call. A client call looks something like this: boto3.client("sqs").create_queue(QueueName="myqueue"), which will be serialized into an HTTP request. This method does the same, without performing the actual request, and with a more low-level interface. An equivalent call would be create_aws_request_context("sqs", "CreateQueue", {"QueueName": "myqueue"}) :param service_name: the AWS service :param action: the action to invoke :param parameters: the invocation parameters :param region: the region name (default is us-east-1) :param endpoint_url: the endpoint to call (defaults to localstack) :return: a RequestContext object that describes this request """ if parameters is None: parameters = {} if region is None: region = config.AWS_REGION_US_EAST_1 service = load_service(service_name) operation = service.operation_model(action) # we re-use botocore internals here to serialize the HTTP request, but don't send it client = aws_stack.connect_to_service(service_name, endpoint_url=endpoint_url, region_name=region) request_context = { "client_region": region, "has_streaming_input": operation.has_streaming_input, "auth_type": operation.auth_type, } request_dict = client._convert_to_request_dict(parameters, operation, context=request_context) aws_request = client._endpoint.create_request(request_dict, operation) context = RequestContext() context.service = service context.operation = operation context.region = region context.request = create_http_request(aws_request) return context
def invoke(self, context: RequestContext) -> HttpResponse: if context.operation and context.service_request: # if the parsed request is already set in the context, re-use them operation, instance = context.operation, context.service_request else: # otherwise, parse the incoming HTTPRequest operation, instance = self.parser.parse(context.request) context.operation = operation try: # Find the operation's handler in the dispatch table if operation.name not in self.dispatch_table: LOG.warning( "missing entry in dispatch table for %s.%s", self.service.service_name, operation.name, ) raise NotImplementedError return self.dispatch_request(context, instance) except ServiceException as e: return self.on_service_exception(context, e) except NotImplementedError: return self.on_not_implemented_error(context)
def test_composite_handler_continues_handler_chain(self): inner1 = mock.MagicMock() inner2 = mock.MagicMock() outer1 = mock.MagicMock() outer2 = mock.MagicMock() response1 = mock.MagicMock() chain = HandlerChain() composite = CompositeHandler() composite.handlers.append(inner1) composite.handlers.append(inner2) chain.request_handlers.append(outer1) chain.request_handlers.append(composite) chain.request_handlers.append(outer2) chain.response_handlers.append(response1) chain.handle(RequestContext(), Response()) outer1.assert_called_once() outer2.assert_called_once() inner1.assert_called_once() inner2.assert_called_once() response1.assert_called_once()
def test_skeleton_e2e_sqs_send_message(): sqs_service = load_service("sqs") skeleton = Skeleton(sqs_service, TestSqsApi()) context = RequestContext() context.account = "test" context.region = "us-west-1" context.service = sqs_service context.request = HttpRequest( **{ "method": "POST", "path": "/", "body": "Action=SendMessage&Version=2012-11-05&QueueUrl=http%3A%2F%2Flocalhost%3A4566%2F000000000000%2Ftf-acc-test-queue&MessageBody=%7B%22foo%22%3A+%22bared%22%7D&DelaySeconds=2", "headers": _get_sqs_request_headers(), }) result = skeleton.invoke(context) # Use the parser from botocore to parse the serialized response response_parser = create_parser(sqs_service.protocol) parsed_response = response_parser.parse( result.to_readonly_response_dict(), sqs_service.operation_model("SendMessage").output_shape) # Test the ResponseMetadata and delete it afterwards assert "ResponseMetadata" in parsed_response assert "RequestId" in parsed_response["ResponseMetadata"] assert len(parsed_response["ResponseMetadata"]["RequestId"]) == 52 assert "HTTPStatusCode" in parsed_response["ResponseMetadata"] assert parsed_response["ResponseMetadata"]["HTTPStatusCode"] == 200 del parsed_response["ResponseMetadata"] # Compare the (remaining) actual payload assert parsed_response == { "MD5OfMessageBody": "String", "MD5OfMessageAttributes": "String", "MD5OfMessageSystemAttributes": "String", "MessageId": "String", "SequenceNumber": "String", }
def test_skeleton_e2e_sqs_send_message_not_implemented(): sqs_service = load_service("sqs") skeleton = Skeleton(sqs_service, TestSqsApiNotImplemented()) context = RequestContext() context.account = "test" context.region = "us-west-1" context.service = sqs_service context.request = HttpRequest( **{ "method": "POST", "path": "/", "body": "Action=SendMessage&Version=2012-11-05&QueueUrl=http%3A%2F%2Flocalhost%3A4566%2F000000000000%2Ftf-acc-test-queue&MessageBody=%7B%22foo%22%3A+%22bared%22%7D&DelaySeconds=2", "headers": _get_sqs_request_headers(), }) result = skeleton.invoke(context) # Use the parser from botocore to parse the serialized response response_parser = create_parser(sqs_service.protocol) parsed_response = response_parser.parse( result.to_readonly_response_dict(), sqs_service.operation_model("SendMessage").output_shape) # Test the ResponseMetadata assert "ResponseMetadata" in parsed_response assert "RequestId" in parsed_response["ResponseMetadata"] assert len(parsed_response["ResponseMetadata"]["RequestId"]) == 52 assert "HTTPStatusCode" in parsed_response["ResponseMetadata"] assert parsed_response["ResponseMetadata"]["HTTPStatusCode"] == 501 # Compare the (remaining) actual eror payload assert "Error" in parsed_response assert parsed_response["Error"] == { "Code": "InternalFailure", "Message": "API action 'SendMessage' for service 'sqs' not yet implemented", }
def test_dispatch_without_args(self): def fn(context): assert type(context) == RequestContext dispatcher = ServiceRequestDispatcher(fn, "SomeAction") dispatcher(RequestContext(), ServiceRequest())