Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: modifyFieldsOnValue schema and tests #1087

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions splunk_add_on_ucc_framework/global_config_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import re
from typing import Any, Dict, List
import logging
import itertools


import jsonschema

Expand Down Expand Up @@ -560,6 +562,158 @@ def _validate_groups(self) -> None:
f"Service {service['name']} uses group field {group_field} which is not defined in entity"
)

def _is_circular(
self,
mods: List[Any],
visited: Dict[str, str],
all_entity_fields: List[Any],
current_field: str,
) -> Dict[str, str]:
"""
Checks if there is circular modification based on visited list and DFS algorithm
"""
DEAD_END = "dead_end"
VISITING = "visited"
visited[current_field] = VISITING

current_field_mods = next(
(mod for mod in mods if mod["fieldId"] == current_field), None
)
if current_field_mods is None:
# no more dependent modification fields
visited[current_field] = DEAD_END
return visited
else:
for influenced_field in current_field_mods["influenced_fields"]:
if influenced_field not in all_entity_fields:
raise GlobalConfigValidatorException(
f"""Modification in field '{current_field}' for not existing field '{influenced_field}'"""
)
if influenced_field == current_field:
raise GlobalConfigValidatorException(
f"""Field '{current_field}' tries to modify itself"""
)
if visited[influenced_field] == VISITING:
raise GlobalConfigValidatorException(
f"""Circular modifications for field '{influenced_field}' in field '{current_field}'"""
)
else:
visited = self._is_circular(
mods, visited, all_entity_fields, influenced_field
)
# all of dependent modifications fields are dead_end
visited[current_field] = DEAD_END
return visited

def _check_if_cilcular(
self,
all_entity_fields: List[Any],
fields_with_mods: List[Any],
modifications: List[Any],
) -> None:
visited = {field: "not_visited" for field in all_entity_fields}

for start_field in fields_with_mods:
# DFS algorithm for all fields with modifications
visited = self._is_circular(
modifications, visited, all_entity_fields, start_field
)

@staticmethod
def _get_mods_data_for_single_entity(
fields_with_mods: List[Any],
all_modifications: List[Any],
entity: Dict[str, Any],
) -> List[Any]:
"""
Add modification entity data to lists and returns them
"""
if "modifyFieldsOnValue" in entity:
influenced_fields = set()
fields_with_mods.append(entity["field"])
for mods in entity["modifyFieldsOnValue"]:
for mod in mods["fieldsToModify"]:
influenced_fields.add(mod["fieldId"])
all_modifications.append(
{"fieldId": entity["field"], "influenced_fields": influenced_fields}
)
return [fields_with_mods, all_modifications]

@staticmethod
def _get_all_entities(
collections: List[Dict[str, Any]],
) -> List[Any]:
all_fields = []

tab_entities: List[Any] = [
el.get("entity") for el in collections if el.get("entity")
]
all_entities = list(itertools.chain.from_iterable(tab_entities))

for entity in all_entities:
if entity["type"] == "oauth":
for oauthType in entity["options"]["auth_type"]:
for oauthEntity in entity["options"][oauthType]:
all_fields.append(oauthEntity)
else:
all_fields.append(entity)

return all_fields

def _get_all_modifiction_data(
self,
collections: List[Dict[str, Any]],
) -> List[Any]:
fields_with_mods: List[Any] = []
all_modifications: List[Any] = []
all_fields: List[str] = []

entities = self._get_all_entities(collections)
for entity in entities:
self._get_mods_data_for_single_entity(
fields_with_mods, all_modifications, entity
)
all_fields.append(entity["field"])

return [fields_with_mods, all_modifications, all_fields]

def _validate_field_modifications(self) -> None:
"""
Validates:
Circular dependencies
If fields try modify itself
If fields try modify unexisting field
"""
pages = self._config["pages"]

if "configuration" in pages:
configuration = pages["configuration"]
tabs = configuration["tabs"]

(
fields_with_mods_config,
all_modifications_config,
all_fields_config,
) = self._get_all_modifiction_data(tabs)

self._check_if_cilcular(
all_fields_config, fields_with_mods_config, all_modifications_config
)

if "inputs" in pages:
inputs = pages["inputs"]
services = inputs["services"]

(
fields_with_mods_inputs,
all_modifications_inputs,
all_fields_inputs,
) = self._get_all_modifiction_data(services)

self._check_if_cilcular(
all_fields_inputs, fields_with_mods_inputs, all_modifications_inputs
)

def validate(self) -> None:
self._validate_config_against_schema()
self._validate_configuration_tab_table_has_name_field()
Expand All @@ -573,3 +727,4 @@ def validate(self) -> None:
self._warn_on_placeholder_usage()
self._validate_checkbox_group()
self._validate_groups()
self._validate_field_modifications()
36 changes: 18 additions & 18 deletions splunk_add_on_ucc_framework/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -446,12 +446,12 @@
},
"requiredWhenVisible": {
"type": "boolean"
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"additionalProperties": false
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"required": ["field", "label", "type"],
Expand Down Expand Up @@ -533,12 +533,12 @@
},
"rowsMax": {
"type": "number"
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"additionalProperties": false
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"required": ["field", "label", "type"],
Expand Down Expand Up @@ -658,12 +658,12 @@
"items": {
"$ref": "#/definitions/ValueLabelPair"
}
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"additionalProperties": false
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"required": ["field", "label", "type", "options"],
Expand Down Expand Up @@ -780,12 +780,12 @@
"delimiter": {
"type": "string",
"maxLength": 1
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"additionalProperties": false
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"required": ["field", "label", "type", "options"],
Expand Down Expand Up @@ -1020,13 +1020,13 @@
"items": {
"$ref": "#/definitions/ValueLabelPair"
}
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"required": ["items"],
"additionalProperties": false
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"required": ["field", "label", "type", "options"],
Expand Down Expand Up @@ -1099,12 +1099,12 @@
"type": "string"
},
"uniqueItems": true
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"additionalProperties": false
},
"modifyFieldsOnValue": {
"$ref": "#/definitions/modifyFieldsOnValue"
}
},
"required": ["field", "label", "type"],
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_global_config_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,45 @@ def test_config_validation_when_error(filename, is_yaml, exception_message):

(msg,) = exc_info.value.args
assert msg == exception_message


def test_config_validation_modifications_on_change():
global_config_path = helpers.get_testdata_file_path(
"valid_config_with_modification_on_value_change.json"
)
global_config = global_config_lib.GlobalConfig(global_config_path, False)

validator = GlobalConfigValidator(helpers.get_path_to_source_dir(), global_config)

with does_not_raise():
validator.validate()


@pytest.mark.parametrize(
"filename,raise_message",
[
(
"invalid_config_with_modification_for_field_itself.json",
"Field 'text1' tries to modify itself",
),
(
"invalid_config_with_modification_for_unexisiting_fields.json",
"Modification in field 'text1' for not existing field 'text2'",
),
(
"invalid_config_with_modification_circular_modifications.json",
"Circular modifications for field 'text1' in field 'text7'",
),
],
)
def test_invalid_config_modifications_correct_raises(filename, raise_message):
global_config_path = helpers.get_testdata_file_path(filename)
global_config = global_config_lib.GlobalConfig(global_config_path, False)

validator = GlobalConfigValidator(helpers.get_path_to_source_dir(), global_config)

with pytest.raises(GlobalConfigValidatorException) as exc_info:
validator.validate()

(msg,) = exc_info.value.args
assert msg == raise_message
Loading
Loading