diff --git a/.github/workflows/test_outdated_versions.yml b/.github/workflows/test_outdated_versions.yml index 4a58d08db..eb538e8f4 100644 --- a/.github/workflows/test_outdated_versions.yml +++ b/.github/workflows/test_outdated_versions.yml @@ -16,7 +16,7 @@ jobs: python-version: [ "3.10" ] responses-version: ["0.13.0", "0.15.0", "0.17.0", "0.19.0", "0.20.0" ] mock-version: [ "3.0.5", "4.0.0", "4.0.3" ] - werkzeug-version: ["2.0.1", "2.1.1"] + werkzeug-version: ["2.0.1", "2.1.1", "2.2.2"] openapi-spec-validator-version: ["0.4.0", "0.5.0"] steps: @@ -37,9 +37,36 @@ jobs: pip install -r requirements-dev.txt pip install responses==${{ matrix.responses-version }} pip install mock==${{ matrix.mock-version }} + pip install flask==${{ matrix.werkzeug-version }} pip install werkzeug==${{ matrix.werkzeug-version }} pip install openapi-spec-validator==${{ matrix.openapi-spec-validator-version }} - name: Run tests run: | pytest -sv tests/test_core ./tests/test_apigateway/test_apigateway_integration.py ./tests/test_s3/test_server.py + + - name: Start MotoServer + run: | + python setup.py sdist + docker run --rm -t --name motoserver -e TEST_SERVER_MODE=true -e AWS_SECRET_ACCESS_KEY=server_secret -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 5000:5000 -v /var/run/docker.sock:/var/run/docker.sock python:3.7-buster /moto/scripts/ci_moto_server.sh & + python scripts/ci_wait_for_server.py + - name: Test ServerMode/Coverage + env: + TEST_SERVER_MODE: ${{ true }} + run: | + pytest -sv tests/test_core tests/test_awslambda tests/test_cloudformation + - name: "Stop MotoServer" + if: always() + run: | + mkdir serverlogs + pwd + ls -la + cp server_output.log serverlogs/server_output.log + docker stop motoserver + - name: Archive TF logs + if: always() + uses: actions/upload-artifact@v3 + with: + name: motoserver + path: | + serverlogs/* diff --git a/moto/apigateway/urls.py b/moto/apigateway/urls.py index 816395a32..461d899bb 100644 --- a/moto/apigateway/urls.py +++ b/moto/apigateway/urls.py @@ -19,8 +19,9 @@ url_paths = { "{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/?$": response.resource_individual, "{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/methods/(?P[^/]+)/?$": response.resource_methods, r"{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/methods/(?P[^/]+)/responses/(?P\d+)$": response.resource_method_responses, - "{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/methods/(?P[^/]+)/integration/?$": response.integrations, - r"{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/methods/(?P[^/]+)/integration/responses/(?P\d+)/?$": response.integration_responses, + r"{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/methods/(?P[^/]+)/integration$": response.integrations, + r"{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/methods/(?P[^/]+)/integration/responses/(?P\d+)$": response.integration_responses, + r"{0}/restapis/(?P[^/]+)/resources/(?P[^/]+)/methods/(?P[^/]+)/integration/responses/(?P\d+)/$": response.integration_responses, "{0}/apikeys$": response.apikeys, "{0}/apikeys/(?P[^/]+)": response.apikey_individual, "{0}/usageplans$": response.usage_plans, diff --git a/moto/apigatewayv2/urls.py b/moto/apigatewayv2/urls.py index 375ca5b3e..bf66afe44 100644 --- a/moto/apigatewayv2/urls.py +++ b/moto/apigatewayv2/urls.py @@ -26,7 +26,9 @@ url_paths = { "{0}/v2/apis/(?P[^/]+)/routes/(?P[^/]+)/routeresponses$": response_v2.route_responses, "{0}/v2/apis/(?P[^/]+)/routes/(?P[^/]+)/routeresponses/(?P[^/]+)$": response_v2.route_response, "{0}/v2/apis/(?P[^/]+)/routes/(?P[^/]+)/requestparameters/(?P[^/]+)$": response_v2.route_request_parameter, - "{0}/v2/tags/(?P.+)$": response_v2.tags, + "{0}/v2/tags/(?P[^/]+)$": response_v2.tags, + "{0}/v2/tags/(?P[^/]+)/apis/(?P[^/]+)$": response_v2.tags, + "{0}/v2/tags/(?P[^/]+)/vpclinks/(?P[^/]+)$": response_v2.tags, "{0}/v2/vpclinks$": response_v2.vpc_links, "{0}/v2/vpclinks/(?P[^/]+)$": response_v2.vpc_link, } diff --git a/moto/appsync/urls.py b/moto/appsync/urls.py index e165c7944..1b078f720 100644 --- a/moto/appsync/urls.py +++ b/moto/appsync/urls.py @@ -16,5 +16,6 @@ url_paths = { "{0}/v1/apis/(?P[^/]+)/apikeys/(?P[^/]+)$": response.api_key_individual, "{0}/v1/apis/(?P[^/]+)/schemacreation$": response.schemacreation, "{0}/v1/tags/(?P.+)$": response.tags, + "{0}/v1/tags/(?P.+)/(?P.+)$": response.tags, "{0}/v1/apis/(?P[^/]+)/types/(?P.+)$": response.types, } diff --git a/moto/awslambda/urls.py b/moto/awslambda/urls.py index 348014251..3d7c310f0 100644 --- a/moto/awslambda/urls.py +++ b/moto/awslambda/urls.py @@ -5,16 +5,18 @@ url_bases = [r"https?://lambda\.(.+)\.amazonaws\.com"] response = LambdaResponse() url_paths = { - r"{0}/(?P[^/]+)/functions/?$": response.root, + r"{0}/(?P[^/]+)/functions$": response.root, + r"{0}/(?P[^/]+)/functions/$": response.root, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/?$": response.function, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/aliases$": response.aliases, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/aliases/(?P[\w_-]+)$": response.alias, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/versions/?$": response.versions, - r"{0}/(?P[^/]+)/event-source-mappings/?$": response.event_source_mappings, + r"{0}/(?P[^/]+)/event-source-mappings/$": response.event_source_mappings, r"{0}/(?P[^/]+)/event-source-mappings/(?P[\w_-]+)/?$": response.event_source_mapping, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/invocations/?$": response.invoke, r"{0}/(?P[^/]+)/functions/(?P.+)/invocations/?$": response.invoke, - r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/invoke-async/?$": response.invoke_async, + r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/invoke-async$": response.invoke_async, + r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/invoke-async/$": response.invoke_async, r"{0}/(?P[^/]+)/tags/(?P.+)": response.tag, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/policy/(?P[\w_-]+)$": response.policy, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/policy/?$": response.policy, @@ -23,7 +25,9 @@ url_paths = { r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/code-signing-config$": response.code_signing_config, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/concurrency/?$": response.function_concurrency, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/url/?$": response.function_url_config, - r"{0}/(?P[^/]+)/layers/?$": response.list_layers, - r"{0}/(?P[^/]+)/layers/(?P[\w_-]+)/versions/?$": response.layers_versions, + r"{0}/(?P[^/]+)/layers$": response.list_layers, + r"{0}/(?P[^/]+)/layers/$": response.list_layers, + r"{0}/(?P[^/]+)/layers/(?P[\w_-]+)/versions$": response.layers_versions, + r"{0}/(?P[^/]+)/layers/(?P[\w_-]+)/versions/$": response.layers_versions, r"{0}/(?P[^/]+)/layers/(?P[\w_-]+)/versions/(?P[\w_-]+)$": response.layers_version, } diff --git a/moto/moto_server/utilities.py b/moto/moto_server/utilities.py index 27b45dcac..bf8e504e9 100644 --- a/moto/moto_server/utilities.py +++ b/moto/moto_server/utilities.py @@ -8,6 +8,8 @@ from werkzeug.routing import BaseConverter class RegexConverter(BaseConverter): # http://werkzeug.pocoo.org/docs/routing/#custom-converters + part_isolating = False + def __init__(self, url_map, *items): super().__init__(url_map) self.regex = items[0] diff --git a/moto/pinpoint/urls.py b/moto/pinpoint/urls.py index 58e801af5..9051ffd51 100644 --- a/moto/pinpoint/urls.py +++ b/moto/pinpoint/urls.py @@ -14,5 +14,6 @@ url_paths = { "{0}/v1/apps/(?P[^/]+)$": response.app, "{0}/v1/apps/(?P[^/]+)/eventstream": response.eventstream, "{0}/v1/apps/(?P[^/]+)/settings$": response.app_settings, - "{0}/v1/tags/(?P.+)$": response.tags, + "{0}/v1/tags/(?P[^/]+)$": response.tags, + "{0}/v1/tags/(?P[^/]+)/(?P[^/]+)$": response.tags, } diff --git a/moto/route53/urls.py b/moto/route53/urls.py index a8679154d..b5b8f0819 100644 --- a/moto/route53/urls.py +++ b/moto/route53/urls.py @@ -15,8 +15,10 @@ def tag_response2(*args, **kwargs): url_paths = { r"{0}/(?P[\d_-]+)/hostedzone$": Route53().list_or_create_hostzone_response, r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)$": Route53().get_or_delete_hostzone_response, - r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/rrset/?$": Route53().rrset_response, - r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/dnssec/?$": Route53().get_dnssec_response, + r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/rrset$": Route53().rrset_response, + r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/rrset/$": Route53().rrset_response, + r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/dnssec$": Route53().get_dnssec_response, + r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/dnssec/$": Route53().get_dnssec_response, r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/associatevpc/?$": Route53().associate_vpc_response, r"{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/disassociatevpc/?$": Route53().disassociate_vpc_response, r"{0}/(?P[\d_-]+)/hostedzonesbyname": Route53().list_hosted_zones_by_name_response, diff --git a/moto/s3/urls.py b/moto/s3/urls.py index a9f989915..52fe69d50 100644 --- a/moto/s3/urls.py +++ b/moto/s3/urls.py @@ -14,7 +14,8 @@ url_paths = { # subdomain bucket "{0}/$": S3ResponseInstance.bucket_response, # subdomain key of path-based bucket - "{0}/(?P[^/]+)/?$": S3ResponseInstance.ambiguous_response, + "{0}/(?P[^/]+)$": S3ResponseInstance.ambiguous_response, + "{0}/(?P[^/]+)/$": S3ResponseInstance.ambiguous_response, # path-based bucket + key "{0}/(?P[^/]+)/(?P.+)": S3ResponseInstance.key_response, # subdomain bucket + key with empty first part of path diff --git a/setup.py b/setup.py index 123b69a39..993a7ae1e 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ install_requires = [ "cryptography>=3.3.1", "requests>=2.5", "xmltodict", - "werkzeug>=0.5,<2.2.0", + "werkzeug>=0.5,!=2.2.0,!=2.2.1", "pytz", "python-dateutil<3.0.0,>=2.1", "responses>=0.13.0", @@ -73,7 +73,7 @@ all_extra_deps = [ _dep_openapi, _setuptools, ] -all_server_deps = all_extra_deps + ["flask<2.2.0", "flask-cors"] +all_server_deps = all_extra_deps + ["flask!=2.2.0,!=2.2.1", "flask-cors"] extras_per_service = {} for service_name in [ diff --git a/tests/test_cloudformation/test_cloudformation_custom_resources.py b/tests/test_cloudformation/test_cloudformation_custom_resources.py index 6c025767a..872e6b893 100644 --- a/tests/test_cloudformation/test_cloudformation_custom_resources.py +++ b/tests/test_cloudformation/test_cloudformation_custom_resources.py @@ -65,8 +65,8 @@ def test_create_custom_lambda_resource(): success, logs = wait_for_log_msg( expected_msg="Status code: 200", log_group=log_group_name ) - with sure.ensure(f"Logs should indicate success: \n{logs}"): - success.should.equal(True) + assert success, f"Logs should indicate success: \n{logs}" + # Verify the correct Output was returned outputs = get_outputs(cf, stack_name) outputs.should.have.length_of(1) diff --git a/tests/test_s3/test_s3_file_handles.py b/tests/test_s3/test_s3_file_handles.py index e033cc9b5..c40939c8a 100644 --- a/tests/test_s3/test_s3_file_handles.py +++ b/tests/test_s3/test_s3_file_handles.py @@ -1,9 +1,10 @@ import gc import warnings -import unittest from functools import wraps +from moto import settings from moto.s3 import models as s3model from moto.s3.responses import S3ResponseInstance +from unittest import SkipTest, TestCase def verify_zero_warnings(f): @@ -24,13 +25,15 @@ def verify_zero_warnings(f): return wrapped -class TestS3FileHandleClosures(unittest.TestCase): +class TestS3FileHandleClosures(TestCase): """ Large Uploads are written to disk for performance reasons These tests verifies that the filehandles are properly closed after specific actions """ def setUp(self) -> None: + if settings.TEST_SERVER_MODE: + raise SkipTest("No point in testing ServerMode, we're not using boto3") self.s3 = s3model.S3Backend("us-west-1", "1234") self.s3.create_bucket("my-bucket", "us-west-1") self.s3.create_bucket("versioned-bucket", "us-west-1")