import copy from unittest import TestCase from parameterized import param, parameterized from samtranslator.model.exceptions import InvalidDocumentException from samtranslator.open_api.open_api import OpenApiEditor from samtranslator.utils.py27hash_fix import Py27Dict _X_INTEGRATION = "x-amazon-apigateway-integration" _X_ANY_METHOD = "x-amazon-apigateway-any-method" # TODO: add a case for swagger and make sure it fails class TestOpenApiEditor_init(TestCase): # OAS3 doesn't support swagger def test_must_raise_on_valid_swagger(self): valid_swagger = { "swagger": "2.0", # "openapi": "2.1.0" "paths": {"/foo": {}, "/bar": {}}, } # missing openapi key word with self.assertRaises(InvalidDocumentException): OpenApiEditor(valid_swagger) def test_must_raise_on_invalid_openapi(self): invalid_openapi = {"paths": {}} # Missing "openapi" keyword with self.assertRaises(InvalidDocumentException): OpenApiEditor(invalid_openapi) def test_must_succeed_on_valid_openapi(self): valid_openapi = {"openapi": "3.0.1", "paths": {"/foo": {}, "/bar": {}}} editor = OpenApiEditor(valid_openapi) self.assertIsNotNone(editor) self.assertEqual(editor.paths, {"/foo": {}, "/bar": {}}) def test_must_fail_on_invalid_openapi_version(self): invalid_openapi = {"openapi": "2.3.0", "paths": {"/foo": {}, "/bar": {}}} with self.assertRaises(InvalidDocumentException): OpenApiEditor(invalid_openapi) def test_must_fail_on_invalid_openapi_version_2(self): invalid_openapi = {"openapi": "3.1.1.1", "paths": {"/foo": {}, "/bar": {}}} with self.assertRaises(InvalidDocumentException): OpenApiEditor(invalid_openapi) def test_must_succeed_on_valid_openapi3(self): valid_openapi = {"openapi": "3.0.1", "paths": {"/foo": {}, "/bar": {}}} editor = OpenApiEditor(valid_openapi) self.assertIsNotNone(editor) self.assertEqual(editor.paths, {"/foo": {}, "/bar": {}}) class TestOpenApiEditor_has_path(TestCase): def setUp(self): self.openapi = { "openapi": "3.0.1", "paths": { "/foo": {"get": {}, "somemethod": {}}, "/bar": {"post": {}, _X_ANY_METHOD: {}}, "badpath": "string value", }, } self.editor = OpenApiEditor(self.openapi) def test_must_find_path_and_method(self): self.assertTrue(self.editor.has_path("/foo")) self.assertTrue(self.editor.has_path("/foo", "get")) self.assertTrue(self.editor.has_path("/foo", "somemethod")) self.assertTrue(self.editor.has_path("/bar")) self.assertTrue(self.editor.has_path("/bar", "post")) def test_must_find_with_method_case_insensitive(self): self.assertTrue(self.editor.has_path("/foo", "GeT")) self.assertTrue(self.editor.has_path("/bar", "POST")) # Only Method is case insensitive. Path is case sensitive self.assertFalse(self.editor.has_path("/FOO")) def test_must_work_with_any_method(self): """ Method name "ANY" is special. It must be converted to the x-amazon style value before search """ self.assertTrue(self.editor.has_path("/bar", "any")) self.assertTrue(self.editor.has_path("/bar", "AnY")) # Case insensitive self.assertTrue(self.editor.has_path("/bar", _X_ANY_METHOD)) self.assertFalse(self.editor.has_path("/foo", "any")) def test_must_not_find_path(self): self.assertFalse(self.editor.has_path("/foo/other")) self.assertFalse(self.editor.has_path("/bar/xyz")) self.assertFalse(self.editor.has_path("/abc")) def test_must_not_find_path_and_method(self): self.assertFalse(self.editor.has_path("/foo", "post")) self.assertFalse(self.editor.has_path("/foo", "abc")) self.assertFalse(self.editor.has_path("/bar", "get")) self.assertFalse(self.editor.has_path("/bar", "xyz")) def test_must_not_fail_on_bad_path(self): self.assertTrue(self.editor.has_path("badpath")) self.assertFalse(self.editor.has_path("badpath", "somemethod")) class TestOpenApiEditor_has_integration(TestCase): def setUp(self): self.openapi = { "openapi": "3.0.1", "paths": { "/foo": { "get": {_X_INTEGRATION: {"a": "b"}}, "post": {"Fn::If": ["Condition", {_X_INTEGRATION: {"a": "b"}}, {"Ref": "AWS::NoValue"}]}, "delete": {"Fn::If": ["Condition", {"Ref": "AWS::NoValue"}, {_X_INTEGRATION: {"a": "b"}}]}, "somemethod": {"foo": "value"}, "emptyintegration": {_X_INTEGRATION: {}}, "badmethod": "string value", } }, } self.editor = OpenApiEditor(self.openapi) def test_must_find_integration(self): self.assertTrue(self.editor.has_integration("/foo", "get")) def test_must_find_integration_with_condition(self): self.assertTrue(self.editor.has_integration("/foo", "post")) def test_must_find_integration_with_condition2(self): self.assertTrue(self.editor.has_integration("/foo", "delete")) def test_must_not_find_integration(self): self.assertFalse(self.editor.has_integration("/foo", "somemethod")) def test_must_not_find_empty_integration(self): self.assertFalse(self.editor.has_integration("/foo", "emptyintegration")) def test_must_handle_bad_value_for_method(self): self.assertFalse(self.editor.has_integration("/foo", "badmethod")) class TestOpenApiEditor_add_path(TestCase): def setUp(self): self.original_openapi = { "openapi": "3.0.1", "paths": {"/foo": {"get": {"a": "b"}}, "/bar": {}, "/badpath": "string value"}, } self.editor = OpenApiEditor(self.original_openapi) @parameterized.expand( [ param("/new", "get", "new path, new method"), param("/foo", "new method", "existing path, new method"), param("/bar", "get", "existing path, new method"), ] ) def test_must_add_new_path_and_method(self, path, method, case): self.assertFalse(self.editor.has_path(path, method)) self.editor.add_path(path, method) self.assertTrue(self.editor.has_path(path, method), "must add for " + case) self.assertEqual(self.editor.openapi["paths"][path][method], {}) def test_must_raise_non_dict_path_values(self): path = "/badpath" method = "get" with self.assertRaises(InvalidDocumentException): self.editor.add_path(path, method) def test_must_skip_existing_path(self): """ Given an existing path/method, this must :return: """ path = "/foo" method = "get" original_value = copy.deepcopy(self.original_openapi["paths"][path][method]) self.editor.add_path(path, method) modified_openapi = self.editor.openapi self.assertEqual(original_value, modified_openapi["paths"][path][method]) class TestOpenApiEditor_add_lambda_integration(TestCase): def setUp(self): self.original_openapi = { "openapi": "3.0.1", "paths": { "/foo": {"post": {"a": [1, 2, "b"], "responses": {"something": "is already here"}}}, "/bar": {"get": {_X_INTEGRATION: {"a": "b"}}}, "/nullmethod": {"get": None}, }, } self.editor = OpenApiEditor(self.original_openapi) def test_must_override_null_path(self): path = "/nullmethod" method = "get" integration_uri = "something" expected = { "responses": {}, _X_INTEGRATION: { "type": "aws_proxy", "httpMethod": "POST", "payloadFormatVersion": "2.0", "uri": integration_uri, }, } self.editor.add_lambda_integration(path, method, integration_uri, Py27Dict(), Py27Dict()) self.assertTrue(self.editor.has_path(path, method)) actual = self.editor.openapi["paths"][path][method] self.assertEqual(expected, actual) def test_must_add_new_integration_to_new_path(self): path = "/newpath" method = "get" integration_uri = "something" expected = { "responses": {}, _X_INTEGRATION: { "type": "aws_proxy", "httpMethod": "POST", "payloadFormatVersion": "2.0", "uri": integration_uri, }, } self.editor.add_lambda_integration(path, method, integration_uri, Py27Dict(), Py27Dict()) self.assertTrue(self.editor.has_path(path, method)) actual = self.editor.openapi["paths"][path][method] self.assertEqual(expected, actual) def test_must_add_new_integration_with_conditions_to_new_path(self): path = "/newpath" method = "get" integration_uri = "something" condition = "condition" expected = { "Fn::If": [ "condition", { "responses": {}, _X_INTEGRATION: { "type": "aws_proxy", "httpMethod": "POST", "payloadFormatVersion": "2.0", "uri": {"Fn::If": ["condition", integration_uri, {"Ref": "AWS::NoValue"}]}, }, }, {"Ref": "AWS::NoValue"}, ] } self.editor.add_lambda_integration(path, method, integration_uri, Py27Dict(), Py27Dict(), condition=condition) self.assertTrue(self.editor.has_path(path, method)) actual = self.editor.openapi["paths"][path][method] self.assertEqual(expected, actual) def test_must_add_new_integration_to_existing_path(self): path = "/foo" method = "post" integration_uri = "something" expected = { # Current values present in the dictionary *MUST* be preserved "a": [1, 2, "b"], # Responses key must be untouched "responses": {"something": "is already here"}, # New values must be added _X_INTEGRATION: { "type": "aws_proxy", "httpMethod": "POST", "payloadFormatVersion": "2.0", "uri": integration_uri, }, } # Just make sure test is working on an existing path self.assertTrue(self.editor.has_path(path, method)) self.editor.add_lambda_integration(path, method, integration_uri, Py27Dict(), Py27Dict()) actual = self.editor.openapi["paths"][path][method] self.assertEqual(expected, actual) class TestOpenApiEditor_iter_on_path(TestCase): def setUp(self): self.original_openapi = {"openapi": "3.0.1", "paths": {"/foo": {}, "/bar": {}, "/baz": "some value"}} self.editor = OpenApiEditor(self.original_openapi) def test_must_iterate_on_paths(self): expected = {"/foo", "/bar", "/baz"} actual = set(list(self.editor.iter_on_path())) self.assertEqual(expected, actual) class TestOpenApiEditor_normalize_method_name(TestCase): @parameterized.expand( [ param("GET", "get", "must lowercase"), param("PoST", "post", "must lowercase"), param("ANY", _X_ANY_METHOD, "must convert any method"), param(None, None, "must skip empty values"), param({"a": "b"}, {"a": "b"}, "must skip non-string values"), param([1, 2], [1, 2], "must skip non-string values"), ] ) def test_must_normalize(self, input, expected, msg): self.assertEqual(expected, OpenApiEditor._normalize_method_name(input), msg) class TestOpenApiEditor_openapi_property(TestCase): def test_must_return_copy_of_openapi(self): input = {"openapi": "3.0.1", "paths": {}} editor = OpenApiEditor(input) self.assertEqual(input, editor.openapi) # They are equal in content input["openapi"] = "3" self.assertEqual("3.0.1", editor.openapi["openapi"]) # Editor works on a diff copy of input editor.add_path("/foo", "get") self.assertEqual({"/foo": {"get": {}}}, editor.openapi["paths"]) self.assertEqual({}, input["paths"]) # Editor works on a diff copy of input class TestOpenApiEditor_is_valid(TestCase): @parameterized.expand( [ param(OpenApiEditor.gen_skeleton()), # Dict can contain any other unrecognized properties param({"openapi": "3.1.1", "paths": {}, "foo": "bar", "baz": "bar"}) # TODO check and update the regex accordingly # Fails for this: param({"openapi": "3.1.10", "paths": {}, "foo": "bar", "baz": "bar"}) ] ) def test_must_work_on_valid_values(self, openapi): self.assertTrue(OpenApiEditor.is_valid(openapi)) @parameterized.expand( [ ({}, "empty dictionary"), ([1, 2, 3], "array data type"), ({"paths": {}}, "missing openapi property"), ({"openapi": "hello"}, "missing paths property"), ({"openapi": "hello", "paths": [1, 2, 3]}, "array value for paths property"), ] ) def test_must_fail_for_invalid_values(self, data, case): self.assertFalse(OpenApiEditor.is_valid(data), f"openapi dictionary with {case} must not be valid") class TestOpenApiEditor_add_auth(TestCase): def setUp(self): self.original_openapi = { "openapi": "3.0.1", "paths": { "/foo": {"get": {_X_INTEGRATION: {"a": "b"}}, "post": {_X_INTEGRATION: {"a": "b"}}}, "/bar": {"get": {_X_INTEGRATION: {"a": "b"}}}, }, } self.editor = OpenApiEditor(self.original_openapi) class TestOpenApiEditor_is_integration_function_logical_id_match(TestCase): def setUp(self): self.original_openapi = { "openapi": "3.0.1", "paths": { "$default": { "x-amazon-apigateway-any-method": { "Fn::If": [ "condition", { "security": [{"OpenIdAuth": ["scope1", "scope2"]}], "isDefaultRoute": True, "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", "uri": { "Fn::If": [ "condition", { "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HttpApiFunction.Arn}/invocations" }, {"Ref": "AWS::NoValue"}, ] }, "payloadFormatVersion": "1.0", }, "responses": {}, }, {"Ref": "AWS::NoValue"}, ] } }, "/bar": {}, "/badpath": "string value", }, } self.editor = OpenApiEditor(self.original_openapi) def test_must_match_integration_function_if_exists(self): self.assertTrue( self.editor.is_integration_function_logical_id_match( OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD, "HttpApiFunction" ), ) self.assertFalse(self.editor.is_integration_function_logical_id_match("/bar", "get", "HttpApiFunction")) class TestOpenApiEdit_add_description(TestCase): def setUp(self): self.original_openapi_with_description = { "openapi": "3.0.1", "paths": {}, "info": {"description": "Existing Description"}, } self.original_openapi_without_description = { "openapi": "3.0.1", "paths": {}, } def test_must_add_description_if_not_defined(self): editor = OpenApiEditor(self.original_openapi_without_description) editor.add_description("New Description") self.assertEqual(editor.openapi["info"]["description"], "New Description") def test_must_not_add_description_if_already_defined(self): editor = OpenApiEditor(self.original_openapi_with_description) editor.add_description("New Description") self.assertEqual(editor.openapi["info"]["description"], "Existing Description") class TestOpenApiEditor_is_integration_function_logical_id_match_with_alias(TestCase): def setUp(self): self.original_openapi = { "openapi": "3.0.1", "paths": { "$default": { "x-amazon-apigateway-any-method": { "Fn::If": [ "condition", { "security": [{"OpenIdAuth": ["scope1", "scope2"]}], "isDefaultRoute": True, "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", "uri": { "Fn::If": [ "condition", { "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HttpApiFunctionAlias}/invocations" }, {"Ref": "AWS::NoValue"}, ] }, "payloadFormatVersion": "1.0", }, "responses": {}, }, {"Ref": "AWS::NoValue"}, ] } }, "/bar": {}, "/badpath": "string value", }, } self.editor = OpenApiEditor(self.original_openapi) def test_no_match_if_alias(self): self.assertFalse( self.editor.is_integration_function_logical_id_match( OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD, "HttpApiFunctionAlias" ), )