From 67e3fc05d6b9cd3aba7122afa98c61c238c341d2 Mon Sep 17 00:00:00 2001 From: Alexandre Girard Date: Wed, 25 Jan 2023 16:26:04 -0800 Subject: [PATCH 1/2] Revert "Extend SATs to capture UI limitations (#21451)" This reverts commit fa2c03ab4ceb4bde6f0c34c8799b771ca4f81a14. --- .../bases/source-acceptance-test/CHANGELOG.md | 3 - .../bases/source-acceptance-test/Dockerfile | 2 +- .../source_acceptance_test/tests/test_core.py | 154 +--------- .../utils/json_schema_helper.py | 11 - .../unit_tests/test_spec.py | 266 ++++-------------- 5 files changed, 61 insertions(+), 375 deletions(-) diff --git a/airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md b/airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md index edb0f51609b65..4660fe1e2186b 100644 --- a/airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md +++ b/airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md @@ -1,8 +1,5 @@ # Changelog -## 0.3.0 -Add various stricter checks for specs (see PR for details). [#21451](https://github.com/airbytehq/airbyte/pull/21451) - ## 0.2.26 Check `future_state` only for incremental streams. [#21248](https://github.com/airbytehq/airbyte/pull/21248) diff --git a/airbyte-integrations/bases/source-acceptance-test/Dockerfile b/airbyte-integrations/bases/source-acceptance-test/Dockerfile index 0243f1d9fe8ba..af3dcd740a74a 100644 --- a/airbyte-integrations/bases/source-acceptance-test/Dockerfile +++ b/airbyte-integrations/bases/source-acceptance-test/Dockerfile @@ -33,7 +33,7 @@ COPY pytest.ini setup.py ./ COPY source_acceptance_test ./source_acceptance_test RUN pip install . -LABEL io.airbyte.version=0.3.0 +LABEL io.airbyte.version=0.2.26 LABEL io.airbyte.name=airbyte/source-acceptance-test ENTRYPOINT ["python", "-m", "pytest", "-p", "source_acceptance_test.plugin", "-r", "fEsx"] diff --git a/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/tests/test_core.py b/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/tests/test_core.py index 5cd5492c24cd2..0928b07b3db80 100644 --- a/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/tests/test_core.py +++ b/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/tests/test_core.py @@ -76,20 +76,12 @@ def secret_property_names_fixture(): ) -DATE_PATTERN = "^[0-9]{2}-[0-9]{2}-[0-9]{4}$" -DATETIME_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2}:[0-9]{2}:[0-9]{2})?$" - - @pytest.mark.default_timeout(10) class TestSpec(BaseTest): spec_cache: ConnectorSpecification = None previous_spec_cache: ConnectorSpecification = None - @pytest.fixture(name="connection_specification", scope="class") - def connection_specification_fixture(self, connector_spec_dict: dict) -> dict: - return connector_spec_dict["connectionSpecification"] - @pytest.fixture(name="skip_backward_compatibility_tests") def skip_backward_compatibility_tests_fixture( self, @@ -227,15 +219,22 @@ def _property_can_store_secret(prop: dict) -> bool: Some fields can not hold a secret by design, others can. Null type as well as boolean can not hold a secret value. A string, a number or an integer type can always store secrets. - Secret objects and arrays can not be rendered correctly in the UI: + Objects and arrays can hold a secret in case they are generic, + meaning their inner structure is not described in details with properties/items. A field with a constant value can not hold a secret as well. """ unsecure_types = {"string", "integer", "number"} type_ = prop["type"] + is_property_generic_object = type_ == "object" and not any( + [prop.get("properties", {}), prop.get("anyOf", []), prop.get("oneOf", []), prop.get("allOf", [])] + ) + is_property_generic_array = type_ == "array" and not any([prop.get("items", []), prop.get("prefixItems", [])]) is_property_constant_value = bool(prop.get("const")) can_store_secret = any( [ isinstance(type_, str) and type_ in unsecure_types, + is_property_generic_object, + is_property_generic_array, isinstance(type_, list) and (set(type_) & unsecure_types), ] ) @@ -253,7 +252,7 @@ def test_secret_is_properly_marked(self, connector_spec_dict: dict, detailed_log secrets_exposed = [] non_secrets_hidden = [] spec_properties = connector_spec_dict["connectionSpecification"]["properties"] - for type_path, type_value in dpath.util.search(spec_properties, "**/type", yielded=True): + for type_path, value in dpath.util.search(spec_properties, "**/type", yielded=True): _, is_property_name_secret = self._is_spec_property_name_secret(type_path, secret_property_names) if not is_property_name_secret: continue @@ -269,7 +268,7 @@ def test_secret_is_properly_marked(self, connector_spec_dict: dict, detailed_log if non_secrets_hidden: properties = "\n".join(non_secrets_hidden) - pytest.fail( + detailed_logger.warning( f"""Some properties are marked with `airbyte_secret` although they probably should not be. Please double check them. If they're okay, please fix this test. {properties}""" @@ -281,139 +280,6 @@ def test_secret_is_properly_marked(self, connector_spec_dict: dict, detailed_log {properties}""" ) - def _fail_on_errors(self, errors: List[str]): - if len(errors) > 0: - pytest.fail("\n".join(errors)) - - def test_property_type_is_not_array(self, connector_specification: dict): - """ - Each field has one or multiple types, but the UI only supports a single type and optionally "null" as a second type. - """ - errors = [] - for type_path, type_value in dpath.util.search(connector_specification, "**/properties/*/type", yielded=True): - if isinstance(type_value, List): - number_of_types = len(type_value) - if number_of_types != 2 and number_of_types != 1: - errors.append( - f"{type_path} is not either a simple type or an array of a simple type plus null: {type_value} (for example: type: [string, null])" - ) - if number_of_types == 2 and type_value[1] != "null": - errors.append( - f"Second type of {type_path} is not null: {type_value}. Type can either be a simple type or an array of a simple type plus null (for example: type: [string, null])" - ) - self._fail_on_errors(errors) - - def test_object_not_empty(self, connector_specification: dict): - """ - Each object field needs to have at least one property as the UI won't be able to show them otherwise. - If the whole spec is empty, it's allowed to have a single empty object at the top level - """ - schema_helper = JsonSchemaHelper(connector_specification) - errors = [] - for type_path, type_value in dpath.util.search(connector_specification, "**/type", yielded=True): - if type_path == "type": - # allow empty root object - continue - if type_value == "object": - property = schema_helper.get_parent(type_path) - if "oneOf" not in property and ("properties" not in property or len(property["properties"]) == 0): - errors.append( - f"{type_path} is an empty object which will not be represented correctly in the UI. Either remove or add specific properties" - ) - self._fail_on_errors(errors) - - def test_array_type(self, connector_specification: dict): - """ - Each array has one or multiple types for its items, but the UI only supports a single type which can either be object, string or an enum - """ - schema_helper = JsonSchemaHelper(connector_specification) - errors = [] - for type_path, type_type in dpath.util.search(connector_specification, "**/type", yielded=True): - property_definition = schema_helper.get_parent(type_path) - if type_type != "array": - # unrelated "items", not an array definition - continue - items_value = property_definition.get("items", None) - if items_value is None: - continue - elif isinstance(items_value, List): - errors.append(f"{type_path} is not just a single item type: {items_value}") - elif items_value.get("type") not in ["object", "string", "number", "integer"] and "enum" not in items_value: - errors.append(f"Items of {type_path} has to be either object or string or define an enum") - self._fail_on_errors(errors) - - def test_forbidden_complex_types(self, connector_specification: dict): - """ - not, anyOf, patternProperties, prefixItems, allOf, if, then, else, dependentSchemas and dependentRequired are not allowed - """ - forbidden_keys = [ - "not", - "anyOf", - "patternProperties", - "prefixItems", - "allOf", - "if", - "then", - "else", - "dependentSchemas", - "dependentRequired", - ] - found_keys = set() - for forbidden_key in forbidden_keys: - for path, value in dpath.util.search(connector_specification, f"**/{forbidden_key}", yielded=True): - found_keys.add(path) - - for forbidden_key in forbidden_keys: - # remove forbidden keys if they are used as properties directly - for path, _value in dpath.util.search(connector_specification, f"**/properties/{forbidden_key}", yielded=True): - found_keys.remove(path) - - if len(found_keys) > 0: - key_list = ", ".join(found_keys) - pytest.fail(f"Found the following disallowed JSON schema features: {key_list}") - - def test_date_pattern(self, connector_specification: dict, detailed_logger): - """ - Properties with format date or date-time should always have a pattern defined how the date/date-time should be formatted - that corresponds with the format the datepicker component is creating. - """ - schema_helper = JsonSchemaHelper(connector_specification) - for format_path, format in dpath.util.search(connector_specification, "**/format", yielded=True): - if not isinstance(format, str): - # format is not a format definition here but a property named format - continue - property_definition = schema_helper.get_parent(format_path) - pattern = property_definition.get("pattern") - if format == "date" and not pattern == DATE_PATTERN: - detailed_logger.warning( - f"{format_path} is defining a date format without the corresponding pattern. Consider setting the pattern to {DATE_PATTERN} to make it easier for users to edit this field in the UI." - ) - if format == "date-time" and not pattern == DATETIME_PATTERN: - detailed_logger.warning( - f"{format_path} is defining a date-time format without the corresponding pattern Consider setting the pattern to {DATETIME_PATTERN} to make it easier for users to edit this field in the UI." - ) - - def test_date_format(self, connector_specification: dict, detailed_logger): - """ - Properties with a pattern that looks like a date should have their format set to date or date-time. - """ - schema_helper = JsonSchemaHelper(connector_specification) - for pattern_path, pattern in dpath.util.search(connector_specification, "**/pattern", yielded=True): - if not isinstance(pattern, str): - # pattern is not a pattern definition here but a property named pattern - continue - if pattern == DATE_PATTERN or pattern == DATETIME_PATTERN: - property_definition = schema_helper.get_parent(pattern_path) - format = property_definition.get("format") - if not format == "date" and pattern == DATE_PATTERN: - detailed_logger.warning( - f"{pattern_path} is defining a pattern that looks like a date without setting the format to `date`. Consider specifying the format to make it easier for users to edit this field in the UI." - ) - if not format == "date-time" and pattern == DATETIME_PATTERN: - detailed_logger.warning( - f"{pattern_path} is defining a pattern that looks like a date-time without setting the format to `date-time`. Consider specifying the format to make it easier for users to edit this field in the UI." - ) - def test_defined_refs_exist_in_json_spec_file(self, connector_spec_dict: dict): """Checking for the presence of unresolved `$ref`s values within each json spec file""" check_result = list(find_all_values_for_key_in_schema(connector_spec_dict, "$ref")) diff --git a/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/utils/json_schema_helper.py b/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/utils/json_schema_helper.py index 400b5af53b6de..7f0c7badb7114 100644 --- a/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/utils/json_schema_helper.py +++ b/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/utils/json_schema_helper.py @@ -6,7 +6,6 @@ from functools import reduce from typing import Any, Dict, List, Mapping, Optional, Set, Text, Union -import dpath.util import pendulum from jsonref import JsonRef @@ -122,16 +121,6 @@ def get_node(self, path: List[str]) -> Any: node = node[segment] return node - def get_parent(self, path: str) -> Any: - """ - Returns the parent dict of a given path within the `obj` dict - """ - absolute_path = f"/{path}" - parent_path, _ = absolute_path.rsplit(sep="/", maxsplit=1) - if parent_path == "": - return self._schema - return dpath.util.get(self._schema, parent_path) - def find_nodes(self, keys: List[str]) -> List[List[str]]: """Find all paths that lead to nodes with the specified keys. diff --git a/airbyte-integrations/bases/source-acceptance-test/unit_tests/test_spec.py b/airbyte-integrations/bases/source-acceptance-test/unit_tests/test_spec.py index 5eb4a6caca797..327876fec4c76 100644 --- a/airbyte-integrations/bases/source-acceptance-test/unit_tests/test_spec.py +++ b/airbyte-integrations/bases/source-acceptance-test/unit_tests/test_spec.py @@ -652,267 +652,102 @@ def test_additional_properties_is_true(connector_spec, expectation): @pytest.mark.parametrize( - "connector_spec, should_fail", + "connector_spec, should_fail, is_warning_logged", ( ( - {"type": "object", "properties": {"api_token": {"type": "string", "airbyte_secret": True}}}, + { + "connectionSpecification": {"type": "object", "properties": {"api_token": {"type": "string", "airbyte_secret": True}}} + }, False, + False ), - ({"type": "object", "properties": {"api_token": {"type": "null"}}}, False), - ({"type": "object", "properties": {"refresh_token": {"type": "boolean", "airbyte_secret": True}}}, True), - ({"type": "object", "properties": {"refresh_token": {"type": ["null", "string"]}}}, True), - ({"type": "object", "properties": {"credentials": {"type": "array", "items": {"type": "string"}}}}, True), - ({"type": "object", "properties": {"auth": {"oneOf": [{"api_token": {"type": "string"}}]}}}, True), ( { - "type": "object", - "properties": {"credentials": {"oneOf": [{"type": "object", "properties": {"api_key": {"type": "string"}}}]}}, + "connectionSpecification": {"type": "object", "properties": {"api_token": {"type": "null"}}} }, - True, + False, + False ), - ({"type": "object", "properties": {"start_date": {"type": ["null", "string"]}}}, False), - ({"type": "object", "properties": {"credentials": {"oneOf": [{"type": "string", "const": "OAuth2.0"}]}}}, False), - ), -) -def test_airbyte_secret(mocker, connector_spec, should_fail): - mocker.patch.object(conftest.pytest, "fail") - t = _TestSpec() - logger = mocker.Mock() - t.test_secret_is_properly_marked( - {"connectionSpecification": connector_spec}, logger, ("api_key", "api_token", "refresh_token", "jwt", "credentials") - ) - if should_fail: - conftest.pytest.fail.assert_called_once() - else: - conftest.pytest.fail.assert_not_called() - - -@pytest.mark.parametrize( - "connector_spec, should_fail", - ( - ({"type": "object", "properties": {"refresh_token": {"type": ["boolean", "string"]}}}, True), - ({"type": "object", "properties": {"refresh_token": {"type": []}}}, True), - ({"type": "object", "properties": {"refresh_token": {"type": ["string"]}}}, False), - ({"type": "object", "properties": {"refresh_token": {"type": "string"}}}, False), - ({"type": "object", "properties": {"refresh_token": {"type": ["boolean", "null"]}}}, False), - ), -) -def test_property_type_is_not_array(mocker, connector_spec, should_fail): - mocker.patch.object(conftest.pytest, "fail") - t = _TestSpec() - t.test_property_type_is_not_array(connector_spec) - if should_fail: - conftest.pytest.fail.assert_called_once() - else: - conftest.pytest.fail.assert_not_called() - - -@pytest.mark.parametrize( - "connector_spec, should_fail", - ( - ({"type": "object", "properties": {"refresh_token": {"type": "boolean", "airbyte_secret": True}}}, False), - ({"type": "object", "properties": {}}, False), - ({"type": "object", "properties": {"jwt": {"type": "object"}}}, True), ( { - "type": "object", - "properties": { - "jwt": { - "type": "object", - "oneOf": [ - {"type": "object", "properties": {"a": {"type": "string"}}}, - {"type": "object", "properties": {"b": {"type": "string"}}}, - ], - } - }, + "connectionSpecification": {"type": "object", "properties": {"refresh_token": {"type": "boolean", "airbyte_secret": True}}} }, False, + True ), ( { - "type": "object", - "properties": { - "jwt": { - "type": "object", - "oneOf": [ - {"type": "object", "properties": {"a": {"type": "string"}}}, - {"type": "object", "properties": {"b": {"type": "string"}}}, - {"type": "object", "properties": {}}, - ], - } - }, + "connectionSpecification": {"type": "object", "properties": {"jwt": {"type": "object"}}} }, True, + False ), - ({"type": "object", "properties": {"jwt": {"type": "object", "properties": {}}}}, True), - ), -) -def test_object_not_empty(mocker, connector_spec, should_fail): - mocker.patch.object(conftest.pytest, "fail") - t = _TestSpec() - t.test_object_not_empty(connector_spec) - if should_fail: - conftest.pytest.fail.assert_called_once() - else: - conftest.pytest.fail.assert_not_called() - - -@pytest.mark.parametrize( - "connector_spec, should_fail", - ( - ({"type": "object", "properties": {"list": {"type": "array"}}}, False), - ({"type": "object", "properties": {"list": {"type": "array", "items": [{"type": "string"}, {"type": "boolean"}]}}}, True), - ({"type": "object", "properties": {"list": {"type": "array", "items": {"type": "string"}}}}, False), - ({"type": "object", "properties": {"list": {"type": "array", "items": {"type": "number"}}}}, False), - ({"type": "object", "properties": {"list": {"type": "array", "items": {"type": "integer"}}}}, False), - ({"type": "object", "properties": {"list": {"type": "array", "items": {"type": "boolean"}}}}, True), - ({"type": "object", "properties": {"list": {"type": "array", "items": {"type": "number", "enum": [1, 2, 3]}}}}, False), - ({"type": "object", "properties": {"list": {"type": "array", "items": {"enum": ["a", "b", "c"]}}}}, False), ( { - "type": "object", - "properties": {"list": {"type": "array", "items": {"type": "object", "properties": {"a": {"type": "string"}}}}}, + "connectionSpecification": {"type": "object", "properties": {"refresh_token": {"type": ["null", "string"]}}} }, - False, + True, + False ), - ({"type": "object", "properties": {"list": {"type": "array", "items": {"type": "boolean"}}}}, True), - ), -) -def test_array_type(mocker, connector_spec, should_fail): - mocker.patch.object(conftest.pytest, "fail") - t = _TestSpec() - t.test_array_type(connector_spec) - if should_fail: - conftest.pytest.fail.assert_called_once() - else: - conftest.pytest.fail.assert_not_called() - - -@pytest.mark.parametrize( - "connector_spec, should_fail", - ( - ({"type": "object", "properties": {"a": {"type": "string"}}}, False), - ({"type": "object", "properties": {"a": {"type": "string", "allOf": [{"type": "string"}, {"maxLength": 5}]}}}, True), - ({"type": "object", "properties": {"allOf": {"type": "string"}}}, False), ( { - "type": "object", - "properties": {"allOf": {"type": "object", "patternProperties": {"^S_": {"type": "string"}, "^I_": {"type": "integer"}}}}, + "connectionSpecification": {"type": "object", "properties": {"credentials": {"type": "array"}}} }, True, + False ), ( { - "type": "object", - "properties": { - "list": { - "type": "array", - "prefixItems": [{"enum": ["Street", "Avenue", "Boulevard"]}, {"enum": ["NW", "NE", "SW", "SE"]}], - } - }, + "connectionSpecification": {"type": "object", "properties": {"credentials": {"type": "array", "items": {"type": "string"}}}} }, True, - ), - ), -) -def test_forbidden_complex_types(mocker, connector_spec, should_fail): - mocker.patch.object(conftest.pytest, "fail") - t = _TestSpec() - t.test_forbidden_complex_types(connector_spec) - if should_fail: - conftest.pytest.fail.assert_called_once() - else: - conftest.pytest.fail.assert_not_called() - - -@pytest.mark.parametrize( - "connector_spec, is_warning_logged", - ( - ({"type": "object", "properties": {"date": {"type": "string"}}}, False), - ({"type": "object", "properties": {"date": {"type": "string", "format": "date"}}}, True), - ({"type": "object", "properties": {"date": {"type": "string", "format": "date-time"}}}, True), - ( - {"type": "object", "properties": {"date": {"type": "string", "format": "date", "pattern": "^[0-9]{2}-[0-9]{2}-[0-9]{4}$"}}}, - False, + False ), ( { - "type": "object", - "properties": { - "date": { - "type": "string", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2}:[0-9]{2}:[0-9]{2})?$", - } - }, + "connectionSpecification": {"type": "object", "properties": {"auth": {"oneOf": [{"api_token": {"type": "string"}}]}}} }, - False, + True, + False ), - ({"type": "object", "properties": {"date": {"type": "string", "pattern": "^[0-9]{2}-[0-9]{2}-[0-9]{4}$"}}}, False), ( { - "type": "object", - "properties": {"date": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2}:[0-9]{2}:[0-9]{2})?$"}}, + "connectionSpecification": {"type": "object", "properties": {"credentials": {"oneOf": [{"type": "object", "properties": {"api_key": {"type": "string"}}}]}}} }, - False, - ), - ), -) -def test_date_pattern(mocker, connector_spec, is_warning_logged): - mocker.patch.object(conftest.pytest, "fail") - logger = mocker.Mock() - t = _TestSpec() - t.test_date_pattern(connector_spec, logger) - conftest.pytest.fail.assert_not_called() - if is_warning_logged: - _, args, _ = logger.warning.mock_calls[0] - msg, *_ = args - assert "Consider setting the pattern" in msg - - -@pytest.mark.parametrize( - "connector_spec, is_warning_logged", - ( - ({"type": "object", "properties": {"date": {"type": "string"}}}, False), - ({"type": "object", "properties": {"format": {"type": "string"}}}, False), - ({"type": "object", "properties": {"date": {"type": "string", "pattern": "[a-z]*"}}}, False), - ( - {"type": "object", "properties": {"date": {"type": "string", "format": "date", "pattern": "^[0-9]{2}-[0-9]{2}-[0-9]{4}$"}}}, - False, + True, + False ), ( { - "type": "object", - "properties": { - "date": { - "type": "string", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2}:[0-9]{2}:[0-9]{2})?$", - } - }, + "connectionSpecification": {"type": "object", "properties": {"start_date": {"type": ["null", "string"]}}} }, False, + False ), - ({"type": "object", "properties": {"date": {"type": "string", "pattern": "^[0-9]{2}-[0-9]{2}-[0-9]{4}$"}}}, True), ( { - "type": "object", - "properties": {"date": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2}:[0-9]{2}:[0-9]{2})?$"}}, + "connectionSpecification": {"type": "object", "properties": {"credentials": {"oneOf": [{"type": "string", "const": "OAuth2.0"}]}}} }, - True, - ), + False, + False + ) ), ) -def test_date_format(mocker, connector_spec, is_warning_logged): +def test_airbyte_secret(mocker, connector_spec, should_fail, is_warning_logged): mocker.patch.object(conftest.pytest, "fail") - logger = mocker.Mock() t = _TestSpec() - t.test_date_format(connector_spec, logger) - conftest.pytest.fail.assert_not_called() + logger = mocker.Mock() + t.test_secret_is_properly_marked(connector_spec, logger, ("api_key", "api_token", "refresh_token", "jwt", "credentials")) + if should_fail: + conftest.pytest.fail.assert_called_once() + else: + conftest.pytest.fail.assert_not_called() if is_warning_logged: _, args, _ = logger.warning.mock_calls[0] msg, *_ = args - assert "Consider specifying the format" in msg + assert "Some properties are marked with `airbyte_secret` although they probably should not be" in msg + else: + logger.warning.assert_not_called() @pytest.mark.parametrize( @@ -922,15 +757,12 @@ def test_date_format(mocker, connector_spec, is_warning_logged): ("properties/start_date/type", "start_date", False), ("properties/credentials/oneOf/1/properties/api_token/type", "api_token", True), ("properties/type", None, False), # root element - ("properties/accounts/items/2/properties/jwt/type", "jwt", True), - ), + ("properties/accounts/items/2/properties/jwt/type", "jwt", True) + ) ) def test_is_spec_property_name_secret(path, expected_name, expected_result): t = _TestSpec() - assert t._is_spec_property_name_secret(path, ("api_key", "api_token", "refresh_token", "jwt", "credentials")) == ( - expected_name, - expected_result, - ) + assert t._is_spec_property_name_secret(path, ("api_key", "api_token", "refresh_token", "jwt", "credentials")) == (expected_name, expected_result) @pytest.mark.parametrize( @@ -943,12 +775,14 @@ def test_is_spec_property_name_secret(path, expected_name, expected_result): ({"type": "number"}, True), ({"type": ["null", "string"]}, True), ({"type": ["null", "boolean"]}, False), - ({"type": "object"}, False), + ({"type": "object"}, True), + # the object itself cannot hold a secret but the inner items can and will be processed separately ({"type": "object", "properties": {"api_key": {}}}, False), - ({"type": "array"}, False), + ({"type": "array"}, True), + # same as object ({"type": "array", "items": {"type": "string"}}, False), - ({"type": "string", "const": "OAuth2.0"}, False), - ), + ({"type": "string", "const": "OAuth2.0"}, False) + ) ) def test_property_can_store_secret(property_def, can_store_secret): t = _TestSpec() From ea66cee74e3420570b859a292c6fc8064778342e Mon Sep 17 00:00:00 2001 From: Alexandre Girard Date: Wed, 25 Jan 2023 16:29:00 -0800 Subject: [PATCH 2/2] bump to 0.4.0 --- .../bases/source-acceptance-test/CHANGELOG.md | 6 ++++++ .../bases/source-acceptance-test/Dockerfile | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md b/airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md index 4660fe1e2186b..3e911bc3b55c4 100644 --- a/airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md +++ b/airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.4.0 +Revert 0.3.0 + +## 0.3.0 +(Broken) Add various stricter checks for specs (see PR for details). [#21451](https://github.com/airbytehq/airbyte/pull/21451) + ## 0.2.26 Check `future_state` only for incremental streams. [#21248](https://github.com/airbytehq/airbyte/pull/21248) diff --git a/airbyte-integrations/bases/source-acceptance-test/Dockerfile b/airbyte-integrations/bases/source-acceptance-test/Dockerfile index af3dcd740a74a..1208930797505 100644 --- a/airbyte-integrations/bases/source-acceptance-test/Dockerfile +++ b/airbyte-integrations/bases/source-acceptance-test/Dockerfile @@ -33,7 +33,7 @@ COPY pytest.ini setup.py ./ COPY source_acceptance_test ./source_acceptance_test RUN pip install . -LABEL io.airbyte.version=0.2.26 +LABEL io.airbyte.version=0.4.0 LABEL io.airbyte.name=airbyte/source-acceptance-test ENTRYPOINT ["python", "-m", "pytest", "-p", "source_acceptance_test.plugin", "-r", "fEsx"]