Skip to content

Commit 63064d9

Browse files
author
Joe Reuter
authored
Extend SATs to capture UI limitations - fixed version (#21903)
* Revert "Revert "Extend SATs to capture UI limitations (#21451)" (#21896)" This reverts commit 74b5dbf. * fix fixture problem
1 parent 2226a2e commit 63064d9

File tree

5 files changed

+371
-61
lines changed

5 files changed

+371
-61
lines changed

airbyte-integrations/bases/source-acceptance-test/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 0.5.0
4+
Re-release of 0.3.0 [#21451](https://github.com/airbytehq/airbyte/pull/21451)
5+
36
## 0.4.0
47
Revert 0.3.0
58

airbyte-integrations/bases/source-acceptance-test/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ COPY pytest.ini setup.py ./
3333
COPY source_acceptance_test ./source_acceptance_test
3434
RUN pip install .
3535

36-
LABEL io.airbyte.version=0.4.0
36+
LABEL io.airbyte.version=0.5.0
3737
LABEL io.airbyte.name=airbyte/source-acceptance-test
3838

3939
ENTRYPOINT ["python", "-m", "pytest", "-p", "source_acceptance_test.plugin", "-r", "fEsx"]

airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/tests/test_core.py

Lines changed: 140 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ def secret_property_names_fixture():
7676
)
7777

7878

79+
DATE_PATTERN = "^[0-9]{2}-[0-9]{2}-[0-9]{4}$"
80+
DATETIME_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2}:[0-9]{2}:[0-9]{2})?$"
81+
82+
7983
@pytest.mark.default_timeout(10)
8084
class TestSpec(BaseTest):
8185

@@ -219,22 +223,15 @@ def _property_can_store_secret(prop: dict) -> bool:
219223
Some fields can not hold a secret by design, others can.
220224
Null type as well as boolean can not hold a secret value.
221225
A string, a number or an integer type can always store secrets.
222-
Objects and arrays can hold a secret in case they are generic,
223-
meaning their inner structure is not described in details with properties/items.
226+
Secret objects and arrays can not be rendered correctly in the UI:
224227
A field with a constant value can not hold a secret as well.
225228
"""
226229
unsecure_types = {"string", "integer", "number"}
227230
type_ = prop["type"]
228-
is_property_generic_object = type_ == "object" and not any(
229-
[prop.get("properties", {}), prop.get("anyOf", []), prop.get("oneOf", []), prop.get("allOf", [])]
230-
)
231-
is_property_generic_array = type_ == "array" and not any([prop.get("items", []), prop.get("prefixItems", [])])
232231
is_property_constant_value = bool(prop.get("const"))
233232
can_store_secret = any(
234233
[
235234
isinstance(type_, str) and type_ in unsecure_types,
236-
is_property_generic_object,
237-
is_property_generic_array,
238235
isinstance(type_, list) and (set(type_) & unsecure_types),
239236
]
240237
)
@@ -252,7 +249,7 @@ def test_secret_is_properly_marked(self, connector_spec_dict: dict, detailed_log
252249
secrets_exposed = []
253250
non_secrets_hidden = []
254251
spec_properties = connector_spec_dict["connectionSpecification"]["properties"]
255-
for type_path, value in dpath.util.search(spec_properties, "**/type", yielded=True):
252+
for type_path, type_value in dpath.util.search(spec_properties, "**/type", yielded=True):
256253
_, is_property_name_secret = self._is_spec_property_name_secret(type_path, secret_property_names)
257254
if not is_property_name_secret:
258255
continue
@@ -268,7 +265,7 @@ def test_secret_is_properly_marked(self, connector_spec_dict: dict, detailed_log
268265

269266
if non_secrets_hidden:
270267
properties = "\n".join(non_secrets_hidden)
271-
detailed_logger.warning(
268+
pytest.fail(
272269
f"""Some properties are marked with `airbyte_secret` although they probably should not be.
273270
Please double check them. If they're okay, please fix this test.
274271
{properties}"""
@@ -280,6 +277,139 @@ def test_secret_is_properly_marked(self, connector_spec_dict: dict, detailed_log
280277
{properties}"""
281278
)
282279

280+
def _fail_on_errors(self, errors: List[str]):
281+
if len(errors) > 0:
282+
pytest.fail("\n".join(errors))
283+
284+
def test_property_type_is_not_array(self, connector_spec: ConnectorSpecification):
285+
"""
286+
Each field has one or multiple types, but the UI only supports a single type and optionally "null" as a second type.
287+
"""
288+
errors = []
289+
for type_path, type_value in dpath.util.search(connector_spec.connectionSpecification, "**/properties/*/type", yielded=True):
290+
if isinstance(type_value, List):
291+
number_of_types = len(type_value)
292+
if number_of_types != 2 and number_of_types != 1:
293+
errors.append(
294+
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])"
295+
)
296+
if number_of_types == 2 and type_value[1] != "null":
297+
errors.append(
298+
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])"
299+
)
300+
self._fail_on_errors(errors)
301+
302+
def test_object_not_empty(self, connector_spec: ConnectorSpecification):
303+
"""
304+
Each object field needs to have at least one property as the UI won't be able to show them otherwise.
305+
If the whole spec is empty, it's allowed to have a single empty object at the top level
306+
"""
307+
schema_helper = JsonSchemaHelper(connector_spec.connectionSpecification)
308+
errors = []
309+
for type_path, type_value in dpath.util.search(connector_spec.connectionSpecification, "**/type", yielded=True):
310+
if type_path == "type":
311+
# allow empty root object
312+
continue
313+
if type_value == "object":
314+
property = schema_helper.get_parent(type_path)
315+
if "oneOf" not in property and ("properties" not in property or len(property["properties"]) == 0):
316+
errors.append(
317+
f"{type_path} is an empty object which will not be represented correctly in the UI. Either remove or add specific properties"
318+
)
319+
self._fail_on_errors(errors)
320+
321+
def test_array_type(self, connector_spec: ConnectorSpecification):
322+
"""
323+
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
324+
"""
325+
schema_helper = JsonSchemaHelper(connector_spec.connectionSpecification)
326+
errors = []
327+
for type_path, type_type in dpath.util.search(connector_spec.connectionSpecification, "**/type", yielded=True):
328+
property_definition = schema_helper.get_parent(type_path)
329+
if type_type != "array":
330+
# unrelated "items", not an array definition
331+
continue
332+
items_value = property_definition.get("items", None)
333+
if items_value is None:
334+
continue
335+
elif isinstance(items_value, List):
336+
errors.append(f"{type_path} is not just a single item type: {items_value}")
337+
elif items_value.get("type") not in ["object", "string", "number", "integer"] and "enum" not in items_value:
338+
errors.append(f"Items of {type_path} has to be either object or string or define an enum")
339+
self._fail_on_errors(errors)
340+
341+
def test_forbidden_complex_types(self, connector_spec: ConnectorSpecification):
342+
"""
343+
not, anyOf, patternProperties, prefixItems, allOf, if, then, else, dependentSchemas and dependentRequired are not allowed
344+
"""
345+
forbidden_keys = [
346+
"not",
347+
"anyOf",
348+
"patternProperties",
349+
"prefixItems",
350+
"allOf",
351+
"if",
352+
"then",
353+
"else",
354+
"dependentSchemas",
355+
"dependentRequired",
356+
]
357+
found_keys = set()
358+
for forbidden_key in forbidden_keys:
359+
for path, value in dpath.util.search(connector_spec.connectionSpecification, f"**/{forbidden_key}", yielded=True):
360+
found_keys.add(path)
361+
362+
for forbidden_key in forbidden_keys:
363+
# remove forbidden keys if they are used as properties directly
364+
for path, _value in dpath.util.search(connector_spec.connectionSpecification, f"**/properties/{forbidden_key}", yielded=True):
365+
found_keys.remove(path)
366+
367+
if len(found_keys) > 0:
368+
key_list = ", ".join(found_keys)
369+
pytest.fail(f"Found the following disallowed JSON schema features: {key_list}")
370+
371+
def test_date_pattern(self, connector_spec: ConnectorSpecification, detailed_logger):
372+
"""
373+
Properties with format date or date-time should always have a pattern defined how the date/date-time should be formatted
374+
that corresponds with the format the datepicker component is creating.
375+
"""
376+
schema_helper = JsonSchemaHelper(connector_spec.connectionSpecification)
377+
for format_path, format in dpath.util.search(connector_spec.connectionSpecification, "**/format", yielded=True):
378+
if not isinstance(format, str):
379+
# format is not a format definition here but a property named format
380+
continue
381+
property_definition = schema_helper.get_parent(format_path)
382+
pattern = property_definition.get("pattern")
383+
if format == "date" and not pattern == DATE_PATTERN:
384+
detailed_logger.warning(
385+
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."
386+
)
387+
if format == "date-time" and not pattern == DATETIME_PATTERN:
388+
detailed_logger.warning(
389+
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."
390+
)
391+
392+
def test_date_format(self, connector_spec: ConnectorSpecification, detailed_logger):
393+
"""
394+
Properties with a pattern that looks like a date should have their format set to date or date-time.
395+
"""
396+
schema_helper = JsonSchemaHelper(connector_spec.connectionSpecification)
397+
for pattern_path, pattern in dpath.util.search(connector_spec.connectionSpecification, "**/pattern", yielded=True):
398+
if not isinstance(pattern, str):
399+
# pattern is not a pattern definition here but a property named pattern
400+
continue
401+
if pattern == DATE_PATTERN or pattern == DATETIME_PATTERN:
402+
property_definition = schema_helper.get_parent(pattern_path)
403+
format = property_definition.get("format")
404+
if not format == "date" and pattern == DATE_PATTERN:
405+
detailed_logger.warning(
406+
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."
407+
)
408+
if not format == "date-time" and pattern == DATETIME_PATTERN:
409+
detailed_logger.warning(
410+
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."
411+
)
412+
283413
def test_defined_refs_exist_in_json_spec_file(self, connector_spec_dict: dict):
284414
"""Checking for the presence of unresolved `$ref`s values within each json spec file"""
285415
check_result = list(find_all_values_for_key_in_schema(connector_spec_dict, "$ref"))

airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/utils/json_schema_helper.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from functools import reduce
77
from typing import Any, Dict, List, Mapping, Optional, Set, Text, Union
88

9+
import dpath.util
910
import pendulum
1011
from jsonref import JsonRef
1112

@@ -121,6 +122,16 @@ def get_node(self, path: List[str]) -> Any:
121122
node = node[segment]
122123
return node
123124

125+
def get_parent(self, path: str) -> Any:
126+
"""
127+
Returns the parent dict of a given path within the `obj` dict
128+
"""
129+
absolute_path = f"/{path}"
130+
parent_path, _ = absolute_path.rsplit(sep="/", maxsplit=1)
131+
if parent_path == "":
132+
return self._schema
133+
return dpath.util.get(self._schema, parent_path)
134+
124135
def find_nodes(self, keys: List[str]) -> List[List[str]]:
125136
"""Find all paths that lead to nodes with the specified keys.
126137

0 commit comments

Comments
 (0)