diff --git a/src/dashboard-front/src/App.vue b/src/dashboard-front/src/App.vue index 0ad0f3763..622d40270 100644 --- a/src/dashboard-front/src/App.vue +++ b/src/dashboard-front/src/App.vue @@ -483,7 +483,7 @@ name: i18n.t('组件API文档'), id: 5, url: 'componentAPI', - enabled: this.GLOBAL_CONFIG.PLATFORM_FEATURE.MENU_ITEM_ESB_API + enabled: this.GLOBAL_CONFIG.PLATFORM_FEATURE.MENU_ITEM_ESB_API_DOC }, { name: this.$t('网关API SDK'), diff --git a/src/dashboard/apigateway/apigateway/apps/feature/views.py b/src/dashboard/apigateway/apigateway/apps/feature/views.py index f42262b2c..c21f49da2 100644 --- a/src/dashboard/apigateway/apigateway/apps/feature/views.py +++ b/src/dashboard/apigateway/apigateway/apps/feature/views.py @@ -37,6 +37,7 @@ def list(self, request, *args, **kwargs): feature_flags.update( { "MENU_ITEM_ESB_API": feature_flags.get("MENU_ITEM_ESB_API", False) and request.user.is_superuser, + "MENU_ITEM_ESB_API_DOC": feature_flags.get("MENU_ITEM_ESB_API", False), } ) diff --git a/src/dashboard/apigateway/apigateway/apps/plugin/migrations/0006_auto_20230620_1512.py b/src/dashboard/apigateway/apigateway/apps/plugin/migrations/0006_auto_20230620_1512.py new file mode 100644 index 000000000..cc1166864 --- /dev/null +++ b/src/dashboard/apigateway/apigateway/apps/plugin/migrations/0006_auto_20230620_1512.py @@ -0,0 +1,45 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions and +# limitations under the License. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +# +# Generated by Django 3.2.18 on 2023-06-20 07:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plugin', '0005_auto_20230227_2006'), + ] + + operations = [ + migrations.AlterField( + model_name='pluginbinding', + name='scope_type', + field=models.CharField(choices=[('stage', '环境'), ('resource', '资源')], db_index=True, max_length=32), + ), + migrations.AlterField( + model_name='pluginconfig', + name='description', + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AlterField( + model_name='pluginconfig', + name='description_en', + field=models.TextField(blank=True, default=None, null=True), + ), + ] diff --git a/src/dashboard/apigateway/apigateway/apps/plugin/models.py b/src/dashboard/apigateway/apigateway/apps/plugin/models.py index c4a059fd1..adf548522 100644 --- a/src/dashboard/apigateway/apigateway/apps/plugin/models.py +++ b/src/dashboard/apigateway/apigateway/apps/plugin/models.py @@ -130,7 +130,7 @@ class PluginConfig(OperatorModelMixin, TimestampedModelMixin): api = models.ForeignKey(Gateway, on_delete=models.CASCADE) name = models.CharField(max_length=64, db_index=True) type = models.ForeignKey(PluginType, null=True, on_delete=models.PROTECT) - description_i18n = I18nProperty(models.TextField(blank=True, default="")) + description_i18n = I18nProperty(models.TextField(default=None, blank=True, null=True)) description = description_i18n.default_field() description_en = description_i18n.field("en") yaml = models.TextField(blank=True, default=None, null=True) diff --git a/src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py b/src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py index ad267c76e..56ac7a0c3 100644 --- a/src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py +++ b/src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py @@ -23,7 +23,8 @@ - apisix 插件的 check_schema 除校验 schema 外,可能还有一些额外的校验,这些插件配置的额外校验,放在此模块处理 """ import re -from typing import ClassVar, Dict +from collections import Counter +from typing import ClassVar, Dict, List, Optional from django.utils.translation import gettext as _ @@ -40,22 +41,48 @@ class BkCorsChecker(BaseChecker): def check(self, yaml_: str): loaded_data = yaml_loads(yaml_) + self._check_allow_origins(loaded_data.get("allow_origins")) + self._check_allow_origins_by_regex(loaded_data.get("allow_origins_by_regex")) + self._check_allow_methods(loaded_data["allow_methods"]) + self._check_headers(loaded_data["allow_headers"], key="allow_headers") + self._check_headers(loaded_data["expose_headers"], key="expose_headers") + if loaded_data.get("allow_credential"): for key in ["allow_origins", "allow_methods", "allow_headers", "expose_headers"]: if loaded_data.get(key) == "*": raise ValueError(_("当 'allow_credential' 为 True 时, {key} 不能为 '*'。").format(key=key)) - if loaded_data.get("allow_origins_by_regex"): - for re_rule in loaded_data["allow_origins_by_regex"]: - try: - re.compile(re_rule) - except Exception: - raise ValueError(_("allow_origins_by_regex 中数据 '{re_rule}' 不是合法的正则表达式。").format(re_rule=re_rule)) - # 非 apisix check_schema 中逻辑,根据业务需要添加的校验逻辑 if not (loaded_data.get("allow_origins") or loaded_data.get("allow_origins_by_regex")): raise ValueError(_("allow_origins, allow_origins_by_regex 不能同时为空。")) + def _check_allow_origins(self, allow_origins: Optional[str]): + if not allow_origins: + return + self._check_duplicate_items(allow_origins.split(","), "allow_origins") + + def _check_allow_methods(self, allow_methods: str): + self._check_duplicate_items(allow_methods.split(","), "allow_methods") + + def _check_headers(self, headers: str, key: str): + self._check_duplicate_items(headers.split(","), key) + + def _check_allow_origins_by_regex(self, allow_origins_by_regex: Optional[str]): + if not allow_origins_by_regex: + return + + # 必须是一个合法的正则表达式 + for re_rule in allow_origins_by_regex: + try: + re.compile(re_rule) + except Exception: + raise ValueError(_("allow_origins_by_regex 中数据 '{re_rule}' 不是合法的正则表达式。").format(re_rule=re_rule)) + + def _check_duplicate_items(self, data: List[str], key: str): + duplicate_items = [item for item, count in Counter(data).items() if count >= 2] + if duplicate_items: + raise ValueError(_("{} 存在重复的元素:{}。").format(key, ", ".join(duplicate_items))) + class PluginConfigYamlChecker: type_code_to_checker: ClassVar[Dict[str, BaseChecker]] = { diff --git a/src/dashboard/apigateway/apigateway/fixtures/plugins.yaml b/src/dashboard/apigateway/apigateway/fixtures/plugins.yaml index 6e438f4bc..0d83c09af 100644 --- a/src/dashboard/apigateway/apigateway/fixtures/plugins.yaml +++ b/src/dashboard/apigateway/apigateway/fixtures/plugins.yaml @@ -124,7 +124,8 @@ "allow_origins": { "description": "允许跨域访问的 Origin,格式为 scheme://host:port,示例如 https://example.com:8081。如果你有多个 Origin,请使用 , 分隔。当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Origin 通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Origin 均通过,但请注意这样存在安全隐患。", "type": "string", - "pattern": "^(|\\*|\\*\\*|null|\\w+://[^,]+(,\\w+://[^,]+)*)$", + "pattern": "^(|\\*|\\*\\*|null|http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+(,http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+)*)$", + "maxLength": 4096, "default": "" }, "allow_origins_by_regex": { @@ -134,7 +135,8 @@ "items": { "type": "string", "minLength": 1, - "maxLength": 4096 + "maxLength": 4096, + "pattern": "^(\\^)?[-a-zA-Z0-9:/\\[\\]\\{\\}\\(\\)\\.\\*\\+\\?\\|\\\\]+(\\$)?$" }, "ui:component": { "name": "bfArray" @@ -144,6 +146,8 @@ "description": "允许跨域访问的 Method,比如:GET,POST 等。如果你有多个 Method,请使用 , 分割。当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Method 通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Method 都通过,但请注意这样存在安全隐患。", "type": "string", "default": "**", + "pattern": "^(\\*|\\*\\*|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)(,(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE))*)$", + "maxLength": 100, "ui:rules": [ "required" ] @@ -152,6 +156,8 @@ "description": "允许跨域访问时请求方携带哪些非 CORS 规范 以外的 Header。如果你有多个 Header,请使用 , 分割。当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Header 通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Header 都通过,但请注意这样存在安全隐患。", "type": "string", "default": "**", + "pattern": "^(\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$", + "maxLength": 4096, "ui:rules": [ "required" ] @@ -159,6 +165,8 @@ "expose_headers": { "description": "允许跨域访问时响应方携带哪些非 CORS 规范 以外的 Header。如果你有多个 Header,请使用 , 分割。当 allow_credential 为 false 时,可以使用 * 来表示允许任意 Header 。你也可以在启用了 allow_credential 后使用 ** 强制允许任意 Header,但请注意这样存在安全隐患。", "type": "string", + "pattern": "^(|\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$", + "maxLength": 4096, "default": "" }, "max_age": { @@ -208,7 +216,7 @@ "allow_origins": { "description": "Origins to allow CORS. Use the scheme://host:port format. For example, https://example.com:8081. If you have multiple origins, use a , to list them. If allow_credential is set to false, you can enable CORS for all origins by using *. If allow_credential is set to true, you can forcefully allow CORS on all origins by using ** but it will pose some security issues.", "type": "string", - "pattern": "^(|\\*|\\*\\*|null|\\w+://[^,]+(,\\w+://[^,]+)*)$", + "pattern": "^(|\\*|\\*\\*|null|http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+(,http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+)*)$", "default": "" }, "allow_origins_by_regex": { @@ -218,7 +226,8 @@ "items": { "type": "string", "minLength": 1, - "maxLength": 4096 + "maxLength": 4096, + "pattern": "^(\\^)?[-a-zA-Z0-9:/\\[\\]\\{\\}\\(\\)\\.\\*\\+\\?\\|\\\\]+(\\$)?$" }, "ui:component": { "name": "bfArray" @@ -228,6 +237,8 @@ "description": "Request methods to enable CORS on. For example GET, POST. Use , to add multiple methods. If allow_credential is set to false, you can enable CORS for all methods by using *. If allow_credential is set to true, you can forcefully allow CORS on all methods by using ** but it will pose some security issues.", "type": "string", "default": "**", + "pattern": "^(\\*|\\*\\*|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)(,(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE))*)$", + "maxLength": 100, "ui:rules": [ "required" ] @@ -236,6 +247,8 @@ "description": "Headers in the request allowed when accessing a cross-origin resource. Use , to add multiple headers. If allow_credential is set to false, you can enable CORS for all request headers by using *. If allow_credential is set to true, you can forcefully allow CORS on all request headers by using ** but it will pose some security issues.", "type": "string", "default": "**", + "pattern": "^(\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$", + "maxLength": 4096, "ui:rules": [ "required" ] @@ -243,6 +256,8 @@ "expose_headers": { "description": "Headers in the response allowed when accessing a cross-origin resource. Use , to add multiple headers. If allow_credential is set to false, you can enable CORS for all response headers by using *. If allow_credential is set to true, you can forcefully allow CORS on all response headers by using ** but it will pose some security issues.", "type": "string", + "pattern": "^(|\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$", + "maxLength": 4096, "default": "" }, "max_age": { diff --git a/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.mo b/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.mo index 6f168c123..ef4c9fd1d 100644 Binary files a/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.mo and b/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.mo differ diff --git a/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.po b/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.po index 3f8ff3b60..ecb703ea8 100644 --- a/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.po +++ b/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-16 15:48+0800\n" +"POT-Creation-Date: 2023-06-21 15:03+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1026,21 +1026,25 @@ msgstr "Plugin" msgid "插件绑定" msgstr "Plugin Binding" -#: apigateway/apps/plugin/plugin/checker.py:46 +#: apigateway/apps/plugin/plugin/checker.py:53 #, python-brace-format msgid "当 'allow_credential' 为 True 时, {key} 不能为 '*'。" msgstr "{key} cannot be '*' when 'allow_credential' is True." -#: apigateway/apps/plugin/plugin/checker.py:53 +#: apigateway/apps/plugin/plugin/checker.py:57 +msgid "allow_origins, allow_origins_by_regex 不能同时为空。" +msgstr "" +"allow_origins, allow_origins_by_regex cannot be empty at the same time." + +#: apigateway/apps/plugin/plugin/checker.py:79 #, python-brace-format msgid "allow_origins_by_regex 中数据 '{re_rule}' 不是合法的正则表达式。" msgstr "" "The '{re_rule}' in allow_origins_by_regex is not a legal regex expression." -#: apigateway/apps/plugin/plugin/checker.py:57 -msgid "allow_origins, allow_origins_by_regex 不能同时为空。" -msgstr "" -"allow_origins, allow_origins_by_regex cannot be empty at the same time." +#: apigateway/apps/plugin/plugin/checker.py:84 +msgid "{} 存在重复的元素:{}。" +msgstr "Duplicate element in {}: {}." #: apigateway/apps/plugin/plugin/convertor.py:71 #, python-brace-format @@ -1102,10 +1106,6 @@ msgstr "The shared micro-gateway instance does not exist." msgid "发布中" msgstr "Releasing" -#: apigateway/apps/release/releasers.py:306 -msgid "配置下发成功" -msgstr "configuration release success" - #: apigateway/apps/release/views.py:51 msgid "当前选择环境未发布版本,请先发布版本到该环境。" msgstr "The stage has not released, please release first." @@ -1920,6 +1920,9 @@ msgstr "App [{value}] does not match the required pattern." msgid "蓝鲸应用【{app_codes}】不匹配要求的模式。" msgstr "App [{app_codes}] does not match the required pattern." +#~ msgid "配置下发成功" +#~ msgstr "configuration release success" + #~ msgid "资源名称【name={name}】重复。" #~ msgstr "Resource name [{name}] already exists." diff --git a/src/dashboard/apigateway/apigateway/tests/apps/feature/test_views.py b/src/dashboard/apigateway/apigateway/tests/apps/feature/test_views.py index 093e30d37..d8443fe7b 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/feature/test_views.py +++ b/src/dashboard/apigateway/apigateway/tests/apps/feature/test_views.py @@ -43,6 +43,7 @@ def test_list(self, settings, request_factory, mocker, faker, is_superuser, expe view = FeatureFlagViewSet.as_view({"get": "list"}) response = view(request) result = get_response_json(response) - assert len(result["data"]) == 2 + assert len(result["data"]) == 3 assert settings.DEFAULT_FEATURE_FLAG == {"MENU_ITEM_ESB_API": True} assert result["data"]["MENU_ITEM_ESB_API"] == expected + assert result["data"]["MENU_ITEM_ESB_API_DOC"] is True diff --git a/src/dashboard/apigateway/apigateway/tests/apps/plugin/plugin/test_checker.py b/src/dashboard/apigateway/apigateway/tests/apps/plugin/plugin/test_checker.py index 78ddbd594..fd52f0bb9 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/plugin/plugin/test_checker.py +++ b/src/dashboard/apigateway/apigateway/tests/apps/plugin/plugin/test_checker.py @@ -78,6 +78,22 @@ def test_check(self, data): "max_age": 100, "allow_credential": False, }, + { + "allow_origins": "http://foo.com", + "allow_methods": "GET,POST,PUT,GET", + "allow_headers": "**", + "expose_headers": "**", + "max_age": 100, + "allow_credential": False, + }, + { + "allow_origins": "http://foo.com", + "allow_methods": "**", + "allow_headers": "x-token,x-token", + "expose_headers": "", + "max_age": 100, + "allow_credential": False, + }, ], ) def test_check__error(self, data): @@ -85,6 +101,76 @@ def test_check__error(self, data): with pytest.raises(ValueError): checker.check(yaml_dumps(data)) + @pytest.mark.parametrize( + "allow_methods", + [ + "*", + "**", + "GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE", + "GET,POST,OPTIONS", + "GET", + ], + ) + def test_check_allow_methods(self, allow_methods): + checker = BkCorsChecker() + assert checker._check_allow_methods(allow_methods) is None + + @pytest.mark.parametrize( + "allow_methods", + [ + "GET,POST,GET", + ], + ) + def test_check_allow_methods__error(self, allow_methods): + checker = BkCorsChecker() + with pytest.raises(ValueError): + checker._check_allow_methods(allow_methods) + + @pytest.mark.parametrize( + "headers", + [ + "Bk-Token", + "Bk-Token,X-Token", + "BK-TOKEN", + ], + ) + def test_check_headers(self, headers): + checker = BkCorsChecker() + assert checker._check_headers(headers, "key") is None + + @pytest.mark.parametrize( + "headers", + [ + "Bk-Token,Bk-Token", + ], + ) + def test_check_headers__error(self, headers): + checker = BkCorsChecker() + with pytest.raises(ValueError): + checker._check_headers(headers, "key") + + @pytest.mark.parametrize( + "data", + [ + ["a", "b"], + ], + ) + def test_check_duplicate_items(self, data): + checker = BkCorsChecker() + result = checker._check_duplicate_items(data, "key") + assert result is None + + @pytest.mark.parametrize( + "data", + [ + ["a", "b", "a"], + ], + ) + def test_check_duplicate_items__error(self, data): + checker = BkCorsChecker() + with pytest.raises(ValueError): + checker._check_duplicate_items(data, "key") + class TestPluginConfigYamlChecker: @pytest.mark.parametrize( diff --git a/src/dashboard/apigateway/apigateway/tests/fixures/__init__.py b/src/dashboard/apigateway/apigateway/tests/fixures/__init__.py new file mode 100644 index 000000000..2941673fe --- /dev/null +++ b/src/dashboard/apigateway/apigateway/tests/fixures/__init__.py @@ -0,0 +1,17 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions and +# limitations under the License. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +# diff --git a/src/dashboard/apigateway/apigateway/tests/fixures/test_plugins_yaml.py b/src/dashboard/apigateway/apigateway/tests/fixures/test_plugins_yaml.py new file mode 100644 index 000000000..22ec5c032 --- /dev/null +++ b/src/dashboard/apigateway/apigateway/tests/fixures/test_plugins_yaml.py @@ -0,0 +1,157 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions and +# limitations under the License. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +# +import re + +import pytest + + +class TestBkCorsPluginForm: + allow_origins_pattern = ( + "^(|\\*|\\*\\*|null|http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+(,http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+)*)$" + ) + + allow_origins_by_regex_pattern = "^(\\^)?[-a-zA-Z0-9:/\\[\\]\\{\\}\\(\\)\\.\\*\\+\\?\\|\\\\]+(\\$)?$" + + allow_methods_pattern = ( + "^(\\*|\\*\\*|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)" + "(,(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE))*)$" + ) + + allow_headers_pattern = "^(\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$" + + expose_headers_pattern = "^(|\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$" + + @pytest.mark.parametrize( + "allow_origins", + [ + "", + "*", + "**", + "null", + "http://example.com", + "https://example.com", + "http://example.com:8080", + "http://example.com:8080,http://foo.com:8000", + "http://[1:1::1]:8080", + ], + ) + def test_allow_origins_pattern(self, allow_origins): + assert re.match(self.allow_origins_pattern, allow_origins) + + @pytest.mark.parametrize( + "allow_origins", + [ + "test", + "http://foo.com/", + "http://foo.com:8080,", + "http://foo.com,test", + "http://foo.com,", + "http://foo.com, http://example.com", + ], + ) + def test_allow_origins_pattern__error(self, allow_origins): + assert not re.match(self.allow_origins_pattern, allow_origins) + + @pytest.mark.parametrize( + "allow_origins_by_regex", + [ + "http://foo.com", + "http://.*.foo.com", + "http://foo.com:8080", + "^http://.*\\.foo\\.com:8000$", + "^https://.*\\.foo\\.com$", + "http://[1:1::1]:8000", + "^http(s)?://.*\\.example\\.com$", + "^http(s)?://.*\\.(foo|example)\\.com$", + "http://.+\\.foo\\.com", + ], + ) + def test_allow_origins_by_regex(self, allow_origins_by_regex): + assert re.match(self.allow_origins_by_regex_pattern, allow_origins_by_regex) + + @pytest.mark.parametrize( + "allow_origins_by_regex", + [ + "", + " ", + "http://foo.com,", + ], + ) + def test_allow_origins_by_regex__error(self, allow_origins_by_regex): + assert not re.match(self.allow_origins_by_regex_pattern, allow_origins_by_regex) + + @pytest.mark.parametrize( + "allow_methods", + [ + "*", + "**", + "GET", + "GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE", + "GET,POST", + ], + ) + def test_allow_methods(self, allow_methods): + assert re.match(self.allow_methods_pattern, allow_methods) + + @pytest.mark.parametrize( + "allow_methods", + [ + "", + "GET,", + "GET,POST,", + "GET, POST", + ], + ) + def test_allow_methods__error(self, allow_methods): + assert not re.match(self.allow_methods_pattern, allow_methods) + + @pytest.mark.parametrize( + "allow_headers", + [ + "*", + "**", + "Bk-Token", + "Bk-Token,Bk-User", + ], + ) + def test_allow_headers(self, allow_headers): + assert re.match(self.allow_headers_pattern, allow_headers) + + @pytest.mark.parametrize( + "allow_headers", + [ + "", + "Bk-Token, Bk-User", + "Bk_Token", + ], + ) + def test_allow_headers__error(self, allow_headers): + assert not re.match(self.allow_headers_pattern, allow_headers) + + @pytest.mark.parametrize( + "expose_headers", + [ + "", + "*", + "**", + "Bk-Token", + "Bk-Token,Bk-User", + ], + ) + def test_expose_headers(self, expose_headers): + assert re.match(self.expose_headers_pattern, expose_headers)