diff --git a/Makefile b/Makefile index 37ab168fe..8d88d96c7 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,10 @@ SHELL := /bin/bash ifeq ($(TEST_SERVER_MODE), true) - # exclude test_iot and test_iotdata for now - # because authentication of iot is very complicated - # exclude test_kinesisvideoarchivedmedia # because testing with moto_server is difficult with data-endpoint - TEST_EXCLUDE := -k 'not (test_iot or test_kinesisvideoarchivedmedia)' + TEST_EXCLUDE := -k 'not test_kinesisvideoarchivedmedia' else TEST_EXCLUDE := endif diff --git a/moto/iot/responses.py b/moto/iot/responses.py index bbb70bc55..c0de60073 100644 --- a/moto/iot/responses.py +++ b/moto/iot/responses.py @@ -425,8 +425,19 @@ class IoTResponse(BaseResponse): self.iot_backend.attach_policy(policy_name=policy_name, target=target) return json.dumps(dict()) + def dispatch_attached_policies(self, request, full_url, headers): + # This endpoint requires specialized handling because it has + # a uri parameter containing forward slashes that is not + # correctly url encoded when we're running in server mode. + # https://github.com/pallets/flask/issues/900 + self.setup_class(request, full_url, headers) + self.querystring["Action"] = ["ListAttachedPolicies"] + target = self.path.partition("/attached-policies/")[-1] + self.querystring["target"] = [unquote(target)] if "%" in target else [target] + return self.call_action() + def list_attached_policies(self): - principal = unquote(self._get_param("target")) + principal = self._get_param("target") # marker = self._get_param("marker") # page_size = self._get_int_param("pageSize") policies = self.iot_backend.list_attached_policies(target=principal) diff --git a/moto/iot/urls.py b/moto/iot/urls.py index 2ad908714..a4d6436d0 100644 --- a/moto/iot/urls.py +++ b/moto/iot/urls.py @@ -7,4 +7,19 @@ url_bases = ["https?://iot.(.+).amazonaws.com"] response = IoTResponse() -url_paths = {"{0}/.*$": response.dispatch} +url_paths = { + # + # Paths for :class:`moto.core.models.MockAWS` + # + # This route requires special handling. + "{0}/attached-policies/(?P.*)$": response.dispatch_attached_policies, + # The remaining routes can be handled by the default dispatcher. + "{0}/.*$": response.dispatch, + # + # (Flask) Paths for :class:`moto.core.models.ServerModeMockAWS` + # + # This route requires special handling. + "{0}/attached-policies/$": response.dispatch_attached_policies, + # The remaining routes can be handled by the default dispatcher. + "{0}/$": response.dispatch, +} diff --git a/moto/iotdata/responses.py b/moto/iotdata/responses.py index 045ed5e59..e9d087518 100644 --- a/moto/iotdata/responses.py +++ b/moto/iotdata/responses.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from moto.core.responses import BaseResponse from .models import iotdata_backends import json +from six.moves.urllib.parse import unquote class IoTDataPlaneResponse(BaseResponse): @@ -29,6 +30,17 @@ class IoTDataPlaneResponse(BaseResponse): payload = self.iotdata_backend.delete_thing_shadow(thing_name=thing_name) return json.dumps(payload.to_dict()) + def dispatch_publish(self, request, full_url, headers): + # This endpoint requires specialized handling because it has + # a uri parameter containing forward slashes that is not + # correctly url encoded when we're running in server mode. + # https://github.com/pallets/flask/issues/900 + self.setup_class(request, full_url, headers) + self.querystring["Action"] = ["Publish"] + topic = self.path.partition("/topics/")[-1] + self.querystring["target"] = [unquote(topic)] if "%" in topic else [topic] + return self.call_action() + def publish(self): topic = self._get_param("topic") qos = self._get_int_param("qos") diff --git a/moto/iotdata/urls.py b/moto/iotdata/urls.py index b3baa66cc..df7e58e44 100644 --- a/moto/iotdata/urls.py +++ b/moto/iotdata/urls.py @@ -7,4 +7,19 @@ url_bases = ["https?://data.iot.(.+).amazonaws.com"] response = IoTDataPlaneResponse() -url_paths = {"{0}/.*$": response.dispatch} +url_paths = { + # + # Paths for :class:`moto.core.models.MockAWS` + # + # This route requires special handling. + "{0}/topics/(?P.*)$": response.dispatch_publish, + # The remaining routes can be handled by the default dispatcher. + "{0}/.*$": response.dispatch, + # + # (Flask) Paths for :class:`moto.core.models.ServerModeMockAWS` + # + # This route requires special handling. + "{0}/topics/$": response.dispatch_publish, + # The remaining routes can be handled by the default dispatcher. + "{0}/$": response.dispatch, +} diff --git a/moto/server.py b/moto/server.py index 0a419c3c4..ecc332f7d 100644 --- a/moto/server.py +++ b/moto/server.py @@ -34,6 +34,13 @@ UNSIGNED_REQUESTS = { } UNSIGNED_ACTIONS = {"AssumeRoleWithSAML": ("sts", "us-east-1")} +# Some services have v4 signing names that differ from the backend service name/id. +SIGNING_ALIASES = { + "eventbridge": "events", + "execute-api": "iot", + "iotdata": "data.iot", +} + class DomainDispatcherApplication(object): """ @@ -74,6 +81,7 @@ class DomainDispatcherApplication(object): try: credential_scope = auth.split(",")[0].split()[1] _, _, region, service, _ = credential_scope.split("/") + service = SIGNING_ALIASES.get(service.lower(), service) except ValueError: # Signature format does not match, this is exceptional and we can't # infer a service-region. A reduced set of services still use @@ -94,11 +102,6 @@ class DomainDispatcherApplication(object): # S3 is the last resort when the target is also unknown service, region = DEFAULT_SERVICE_REGION - if service == "EventBridge": - # Go SDK uses 'EventBridge' in the SigV4 request instead of 'events' - # see https://github.com/spulec/moto/issues/3494 - service = "events" - if service == "dynamodb": if environ["HTTP_X_AMZ_TARGET"].startswith("DynamoDBStreams"): host = "dynamodbstreams" diff --git a/tests/test_iot/test_server.py b/tests/test_iot/test_server.py index b04f4d8ea..727331deb 100644 --- a/tests/test_iot/test_server.py +++ b/tests/test_iot/test_server.py @@ -1,5 +1,9 @@ from __future__ import unicode_literals +import json +from six.moves.urllib.parse import quote + +import pytest import sure # noqa import moto.server as server @@ -17,4 +21,32 @@ def test_iot_list(): # just making sure that server is up res = test_client.get("/things") - res.status_code.should.equal(404) + res.status_code.should.equal(200) + + +@pytest.mark.parametrize( + "url_encode_arn", + [ + pytest.param(True, id="Target Arn in Path is URL encoded"), + pytest.param(False, id="Target Arn in Path is *not* URL encoded"), + ], +) +@mock_iot +def test_list_attached_policies(url_encode_arn): + backend = server.create_backend_app("iot") + test_client = backend.test_client() + + result = test_client.post("/keys-and-certificate?setAsActive=true") + result_dict = json.loads(result.data.decode("utf-8")) + certificate_arn = result_dict["certificateArn"] + + test_client.post("/policies/my-policy", json={"policyDocument": {}}) + test_client.put("/target-policies/my-policy", json={"target": certificate_arn}) + + if url_encode_arn: + certificate_arn = quote(certificate_arn, safe="") + + result = test_client.post("/attached-policies/{}".format(certificate_arn)) + result.status_code.should.equal(200) + result_dict = json.loads(result.data.decode("utf-8")) + result_dict["policies"][0]["policyName"].should.equal("my-policy") diff --git a/tests/test_iotdata/test_server.py b/tests/test_iotdata/test_server.py index bbced67b6..eb10022b8 100644 --- a/tests/test_iotdata/test_server.py +++ b/tests/test_iotdata/test_server.py @@ -1,5 +1,8 @@ from __future__ import unicode_literals +from six.moves.urllib.parse import quote + +import pytest import sure # noqa import moto.server as server @@ -19,3 +22,22 @@ def test_iotdata_list(): thing_name = "nothing" res = test_client.get("/things/{}/shadow".format(thing_name)) res.status_code.should.equal(404) + + +@pytest.mark.parametrize( + "url_encode_topic", + [ + pytest.param(True, id="Topic in Path is URL encoded"), + pytest.param(False, id="Topic in Path is *not* URL encoded"), + ], +) +@mock_iotdata +def test_publish(url_encode_topic): + backend = server.create_backend_app("iot-data") + test_client = backend.test_client() + + topic = "test/topic" + topic_for_path = quote(topic, safe="") if url_encode_topic else topic + + result = test_client.post("/topics/{}".format(topic_for_path)) + result.status_code.should.equal(200)