diff --git a/docs/advanced/custom_rest_handler.md b/docs/advanced/custom_rest_handler.md index c84c6c16f..e510e94c8 100644 --- a/docs/advanced/custom_rest_handler.md +++ b/docs/advanced/custom_rest_handler.md @@ -71,6 +71,7 @@ def _validate_organization(organization_id, organization_api_key): # Some code to validate the API key. # Should return nothing if the configuration is valid. # Should raise an exception splunktaucclib.rest_handler.error.RestError if the configuration is not valid. + ... class CustomRestHandler(AdminExternalHandler): @@ -105,3 +106,172 @@ if __name__ == "__main__": handler=CustomRestHandler, ) ``` + +### Native support from UCC + +> UCC 5.18.0 natively supports custom REST handlers for the modular inputs. + +One of the common scenarios is to delete a checkpoint after you are deleting an +input in the Inputs page. Otherwise, users may face the wierd consequences if +they create an input with the same name as the input that was deleted and this +newly created input will be reusing the old checkpoint because the names of +the inputs are the same. We would like to avoid this situation in the add-on. + +This can be done without a need to modify the REST handler code generated +automatically by running `ucc-gen`. + +Below is the automatically generated REST handler code for a modular input REST +handler. + +```python + +import import_declare_test + +from splunktaucclib.rest_handler.endpoint import ( + field, + validator, + RestModel, + DataInputModel, +) +from splunktaucclib.rest_handler import admin_external, util +from splunktaucclib.rest_handler.admin_external import AdminExternalHandler +import logging + +util.remove_http_proxy_env_vars() + + +fields = [ + field.RestField( + 'interval', + required=True, + encrypted=False, + default=None, + validator=validator.Pattern( + regex=r"""^\-[1-9]\d*$|^\d*$""", + ) + ), + + field.RestField( + 'disabled', + required=False, + validator=None + ) + +] +model = RestModel(fields, name=None) + + + +endpoint = DataInputModel( + 'example_input_one', + model, +) + + +if __name__ == '__main__': + logging.getLogger().addHandler(logging.NullHandler()) + admin_external.handle( + endpoint, + handler=AdminExternalHandler, + ) +``` + +New file needs to be created in the `bin` folder of the add-on. Let's call it +`splunk_ta_uccexample_delete_checkpoint_rh.py` (name can be different). + +And put the following content into the file. + +```python +import import_declare_test + +from splunktaucclib.rest_handler.admin_external import AdminExternalHandler + + +class CustomRestHandlerDeleteCheckpoint(AdminExternalHandler): + def __init__(self, *args, **kwargs): + AdminExternalHandler.__init__(self, *args, **kwargs) + + def handleList(self, confInfo): + AdminExternalHandler.handleList(self, confInfo) + + def handleEdit(self, confInfo): + AdminExternalHandler.handleEdit(self, confInfo) + + def handleCreate(self, confInfo): + AdminExternalHandler.handleCreate(self, confInfo) + + def handleRemove(self, confInfo): + # Add your code here to delete the checkpoint! + AdminExternalHandler.handleRemove(self, confInfo) +``` + +Then, in globalConfig file you need to change the behaviour of the UCC to reuse +the REST handler that was just created. + +``` +{ + "name": "example_input_one", + "restHandlerModule": "splunk_ta_uccexample_delete_checkpoint_rh", <----- new field + "restHandlerClass": "CustomRestHandlerDeleteCheckpoint", <----- new field + "entity": [ + "..." + ], + "title": "Example Input One" +} +``` + +After `ucc-gen` command is executed again, the generated REST handler for this +input will be changed to the following. + +```python + +import import_declare_test + +from splunktaucclib.rest_handler.endpoint import ( + field, + validator, + RestModel, + DataInputModel, +) +from splunktaucclib.rest_handler import admin_external, util +from splunk_ta_uccexample_delete_checkpoint_rh import CustomRestHandlerDeleteCheckpoint # <----- changed +import logging + +util.remove_http_proxy_env_vars() + + +fields = [ + field.RestField( + 'interval', + required=True, + encrypted=False, + default=None, + validator=validator.Pattern( + regex=r"""^\-[1-9]\d*$|^\d*$""", + ) + ), + + field.RestField( + 'disabled', + required=False, + validator=None + ) + +] +model = RestModel(fields, name=None) + + + +endpoint = DataInputModel( + 'example_input_one', + model, +) + + +if __name__ == '__main__': + logging.getLogger().addHandler(logging.NullHandler()) + admin_external.handle( + endpoint, + handler=CustomRestHandlerDeleteCheckpoint, # <----- changed + ) +``` diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 000000000..72c15c10c --- /dev/null +++ b/docs/components.md @@ -0,0 +1,104 @@ +# Components supported by UCC + +Components are used by UCC to render the Inputs and Configuration pages. Here is +the list of the supported components. + +## `custom` + +TBD + +## `text` + +TBD + +## `textarea` + +Underlying `@splunk/react-ui` component: [`TextArea`](https://splunkui.splunk.com/Packages/react-ui/TextArea). + +`textarea` component is very similar to `text` component, but allows to have a +multi-line input for text. + +Example usage below: + +```json +{ + "type": "textarea", + "label": "Textarea Field", + "field": "textarea_field", + "help": "Help message", + "options": { + "rowsMin": 3, + "rowsMax": 15 + }, + "required": true +} +``` + +This is how it looks like in the UI: + +![image](images/components/textarea_component_example.png) + +## `singleSelect` + +TBD + +## `checkbox` + +TBD + +## `multipleSelect` + +TBD + +## `radio` + +TBD + +## `placeholder` + +TBD + +## `oauth` + +TBD + +## `helpLink` + +TBD + +## `file` + +Underlying `@splunk/react-ui` component: [`File`](https://splunkui.splunk.com/Packages/react-ui/File). + +The current implementation of the `file` component only supports `JSON` files +and accepts only 1 file (can be dragged into). + +Usage example below: + +```json +{ + "type": "file", + "label": "SA certificate", + "help": "Upload service account's certificate", + "field": "service_account", + "options": { + "fileSupportMessage": "Support message" + }, + "validators": [ + { + "type": "file", + "supportedFileTypes": [ + "json" + ] + } + ], + "encrypted": true, + "required": true +} +``` + +> Note: `validators` field should be present for the file input exactly as it is in the example above. + +This is how it looks like in the UI: + +![image](images/components/file_component_example.png) diff --git a/docs/images/components/file_component_example.png b/docs/images/components/file_component_example.png new file mode 100644 index 000000000..6bec9e451 Binary files /dev/null and b/docs/images/components/file_component_example.png differ diff --git a/docs/images/components/textarea_component_example.png b/docs/images/components/textarea_component_example.png new file mode 100644 index 000000000..62fc0c0c2 Binary files /dev/null and b/docs/images/components/textarea_component_example.png differ diff --git a/get-ucc-ui.sh b/get-ucc-ui.sh index 7de21bd2e..bd2fc8892 100755 --- a/get-ucc-ui.sh +++ b/get-ucc-ui.sh @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -wget https://github.com/splunk/addonfactory-ucc-base-ui/releases/download/v1.15.1/splunk-ucc-ui.tgz +wget https://github.com/splunk/addonfactory-ucc-base-ui/releases/download/v1.18.0/splunk-ucc-ui.tgz diff --git a/mkdocs.yml b/mkdocs.yml index ee15f3a26..9bdd0b87c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,6 +24,7 @@ theme: nav: - Home: "index.md" - How to use: "how_to_use.md" + - Components: "components.md" - Example: "example.md" - Tabs: "tabs.md" - Custom UI Extensions: diff --git a/splunk_add_on_ucc_framework/commands/build.py b/splunk_add_on_ucc_framework/commands/build.py index cf39f704b..5a43f96a5 100644 --- a/splunk_add_on_ucc_framework/commands/build.py +++ b/splunk_add_on_ucc_framework/commands/build.py @@ -34,12 +34,16 @@ meta_conf, utils, ) +from splunk_add_on_ucc_framework.commands.rest_builder import global_config +from splunk_add_on_ucc_framework.commands.rest_builder.builder import RestBuilder +from splunk_add_on_ucc_framework.commands.rest_builder.global_config import ( + GlobalConfigBuilderSchema, +) from splunk_add_on_ucc_framework.install_python_libraries import ( SplunktaucclibNotFound, install_python_libraries, ) from splunk_add_on_ucc_framework.start_alert_build import alert_build -from splunk_add_on_ucc_framework.uccrestbuilder import build, global_config logger = logging.getLogger("ucc_gen") @@ -129,7 +133,9 @@ def _replace_token(ta_name, outputdir): f.write(s) -def _generate_rest(ta_name, scheme, import_declare_name, outputdir): +def _generate_rest( + ta_name, scheme: GlobalConfigBuilderSchema, import_declare_name, outputdir +): """ Build REST for Add-on. @@ -139,17 +145,11 @@ def _generate_rest(ta_name, scheme, import_declare_name, outputdir): import_declare_name (str): Name of import_declare_* file. outputdir (str): output directory. """ - rest_handler_module = "splunktaucclib.rest_handler.admin_external" - rest_handler_class = "AdminExternalHandler" - - build( - scheme, - rest_handler_module, - rest_handler_class, - os.path.join(outputdir, ta_name), - post_process=global_config.GlobalConfigPostProcessor(), - import_declare_name=import_declare_name, - ) + builder_obj = RestBuilder(scheme, os.path.join(outputdir, ta_name)) + builder_obj.build() + post_process = global_config.GlobalConfigPostProcessor() + post_process(builder_obj, scheme, import_declare_name=import_declare_name) + return builder_obj def _is_oauth_configured(ta_tabs): diff --git a/splunk_add_on_ucc_framework/commands/rest_builder/__init__.py b/splunk_add_on_ucc_framework/commands/rest_builder/__init__.py new file mode 100644 index 000000000..72d450974 --- /dev/null +++ b/splunk_add_on_ucc_framework/commands/rest_builder/__init__.py @@ -0,0 +1,15 @@ +# +# Copyright 2021 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/builder.py b/splunk_add_on_ucc_framework/commands/rest_builder/builder.py similarity index 90% rename from splunk_add_on_ucc_framework/uccrestbuilder/builder.py rename to splunk_add_on_ucc_framework/commands/rest_builder/builder.py index 1444a78de..028675242 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/builder.py +++ b/splunk_add_on_ucc_framework/commands/rest_builder/builder.py @@ -13,18 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # - - import os import os.path as op -from splunk_add_on_ucc_framework.uccrestbuilder.rest_conf import RestmapConf, WebConf - -__all__ = ["RestBuilderError", "RestBuilder"] - +from splunk_add_on_ucc_framework.commands.rest_builder.global_config import ( + GlobalConfigBuilderSchema, +) +from splunk_add_on_ucc_framework.rest_map_conf import RestmapConf +from splunk_add_on_ucc_framework.web_conf import WebConf -class RestBuilderError(Exception): - pass +__all__ = ["RestBuilder"] class _RestBuilderOutput: @@ -58,18 +56,18 @@ def save(self): class RestBuilder: - def __init__(self, schema, handler, output_path, *args, **kwargs): + def __init__( + self, schema: GlobalConfigBuilderSchema, output_path: str, *args, **kwargs + ): """ :param schema: :param schema: RestSchema - :param handler: :param output_path: :param args: :param kwargs: """ self._schema = schema - self._handler = handler self._output_path = output_path self._args = args self._kwargs = kwargs @@ -116,7 +114,7 @@ def build(self): self.output.put( self.output.bin, endpoint.rh_name + ".py", - endpoint.generate_rh(self._handler), + endpoint.generate_rh(), ) self.output.put( diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/__init__.py b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/__init__.py similarity index 100% rename from splunk_add_on_ucc_framework/uccrestbuilder/endpoint/__init__.py rename to splunk_add_on_ucc_framework/commands/rest_builder/endpoint/__init__.py diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/base.py b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/base.py similarity index 87% rename from splunk_add_on_ucc_framework/uccrestbuilder/endpoint/base.py rename to splunk_add_on_ucc_framework/commands/rest_builder/endpoint/base.py index 2edfd1990..3227c75ae 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/base.py +++ b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/base.py @@ -22,6 +22,8 @@ "indent", ] +from typing import List, Sequence + class RestEntityBuilder: @@ -32,7 +34,7 @@ class RestEntityBuilder: ] model{name_rh} = RestModel(fields{name_rh}, name={name}) """ - _disabled_feild_template = """ + _disabled_field_template = """ field.RestField( 'disabled', required=False, @@ -81,7 +83,7 @@ def generate_rh(self): or entity_builder == "SingleModelEntityBuilder" and self._conf_name ): - fields.append(self._disabled_feild_template) + fields.append(self._disabled_field_template) fields_lines = ", \n".join(fields) return self._rh_template.format( fields=indent(fields_lines), @@ -104,6 +106,8 @@ def __init__(self, name, namespace, **kwargs): self._rest_handler_name = kwargs.get("rest_handler_name") else: self._rest_handler_name = f"{self._namespace}_rh_{self._name}" + self._rest_handler_module = kwargs.get("rest_handler_module") + self._rest_handler_class = kwargs.get("rest_handler_class") @property def name(self): @@ -121,6 +125,14 @@ def conf_name(self): def rh_name(self): return self._rest_handler_name + @property + def rh_module(self): + return self._rest_handler_module + + @property + def rh_class(self): + return self._rest_handler_class + @property def entities(self): return self._entities @@ -128,7 +140,7 @@ def entities(self): def add_entity(self, entity): self._entities.append(entity) - def actions(self): + def actions(self) -> List[str]: raise NotImplementedError() def generate_spec(self): @@ -139,11 +151,11 @@ def generate_default_conf(self): specs = [entity.generate_spec(True) for entity in self._entities] return "\n\n".join(specs) - def generate_rh(self, handler): + def generate_rh(self) -> str: raise NotImplementedError() -def quote_string(value): +def quote_string(value) -> str: """ Quote a string :param value: @@ -155,7 +167,7 @@ def quote_string(value): return value -def quote_regex(value): +def quote_regex(value) -> str: """ Quote a regex :param value: @@ -167,7 +179,7 @@ def quote_regex(value): return value -def indent(lines, spaces=1): +def indent(lines: Sequence[str], spaces: int = 1) -> str: """ Indent code block. diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/datainput.py b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/datainput.py similarity index 85% rename from splunk_add_on_ucc_framework/uccrestbuilder/endpoint/datainput.py rename to splunk_add_on_ucc_framework/commands/rest_builder/endpoint/datainput.py index abdfd3123..a1c21c58f 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/datainput.py +++ b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/datainput.py @@ -13,9 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from typing import List - -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.single_model import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.single_model import ( RestEndpointBuilder, RestEntityBuilder, ) @@ -49,7 +49,7 @@ class DataInputEndpointBuilder(RestEndpointBuilder): DataInputModel, ) from splunktaucclib.rest_handler import admin_external, util -from {handler_module} import {handler_name} +from {handler_module} import {handler_class} import logging util.remove_http_proxy_env_vars() @@ -67,7 +67,7 @@ class DataInputEndpointBuilder(RestEndpointBuilder): logging.getLogger().addHandler(logging.NullHandler()) admin_external.handle( endpoint, - handler={handler_name}, + handler={handler_class}, ) """ @@ -79,14 +79,14 @@ def __init__(self, name, namespace, input_type, **kwargs): def conf_name(self): return "inputs" - def actions(self): + def actions(self) -> List[str]: return ["edit", "list", "remove", "create"] - def generate_rh(self, handler): + def generate_rh(self) -> str: entity = self._entities[0] return self._rh_template.format( - handler_module=handler.module, - handler_name=handler.name, + handler_module=self.rh_module, + handler_class=self.rh_class, entity=entity.generate_rh(), input_type=self.input_type, ) diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/field.py b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/field.py similarity index 88% rename from splunk_add_on_ucc_framework/uccrestbuilder/endpoint/field.py rename to splunk_add_on_ucc_framework/commands/rest_builder/endpoint/field.py index fa42c2b1a..8295b35c7 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/field.py +++ b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/field.py @@ -15,7 +15,7 @@ # -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.base import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import ( indent, quote_string, ) @@ -39,17 +39,17 @@ def __init__(self, name, required, encrypted, default, validator): self._default = default self._validator = validator - def generate_spec(self): + def generate_spec(self) -> str: return self._kv_template.format( name=self._name, value="", ) - def _indent_validator(self): + def _indent_validator(self) -> str: validator = indent(self._validator) return validator[4:] - def generate_rh(self): + def generate_rh(self) -> str: return self._rh_template.format( name=quote_string(self._name), required=self._required, diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/multiple_model.py b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/multiple_model.py similarity index 81% rename from splunk_add_on_ucc_framework/uccrestbuilder/endpoint/multiple_model.py rename to splunk_add_on_ucc_framework/commands/rest_builder/endpoint/multiple_model.py index f632c1729..dd052972c 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/multiple_model.py +++ b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/multiple_model.py @@ -13,10 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from typing import List - -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.base import indent -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.single_model import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import indent +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.single_model import ( RestEndpointBuilder, RestEntityBuilder, ) @@ -46,7 +46,7 @@ class MultipleModelEndpointBuilder(RestEndpointBuilder): MultipleModel, ) from splunktaucclib.rest_handler import admin_external, util -from {handler_module} import {handler_name} +from {handler_module} import {handler_class} import logging util.remove_http_proxy_env_vars() @@ -65,20 +65,20 @@ class MultipleModelEndpointBuilder(RestEndpointBuilder): logging.getLogger().addHandler(logging.NullHandler()) admin_external.handle( endpoint, - handler={handler_name}, + handler={handler_class}, ) """ - def actions(self): + def actions(self) -> List[str]: return ["edit", "list"] - def generate_rh(self, handler): + def generate_rh(self) -> str: entities = [entity.generate_rh() for entity in self._entities] models = ["model" + entity.name_rh for entity in self._entities] models_lines = ", \n".join(models) return self._rh_template.format( - handler_module=handler.module, - handler_name=handler.name, + handler_module=self.rh_module, + handler_class=self.rh_class, entities="\n".join(entities), models=indent(models_lines, 2), conf_name=self.conf_name, diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/oauth_model.py b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/oauth_model.py similarity index 87% rename from splunk_add_on_ucc_framework/uccrestbuilder/endpoint/oauth_model.py rename to splunk_add_on_ucc_framework/commands/rest_builder/endpoint/oauth_model.py index a843481f3..f70369a89 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/oauth_model.py +++ b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/oauth_model.py @@ -13,9 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from typing import List - -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.base import RestEndpointBuilder +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import ( + RestEndpointBuilder, +) """ This class is used to generate the endpoint for getting access token from @@ -38,14 +40,14 @@ def __init__(self, name, j2_env, namespace, **kwargs): Action will return the possible action for the endpoint """ - def actions(self): + def actions(self) -> List[str]: return ["edit"] """ This will actually populate the jinja template with the token values and return it """ - def generate_rh(self, handler): + def generate_rh(self) -> str: return self.j2_env.get_template("oauth.template").render( app_name=self._app_name, ) diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/single_model.py b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/single_model.py similarity index 84% rename from splunk_add_on_ucc_framework/uccrestbuilder/endpoint/single_model.py rename to splunk_add_on_ucc_framework/commands/rest_builder/endpoint/single_model.py index 69d07b3dc..78e534d51 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/endpoint/single_model.py +++ b/splunk_add_on_ucc_framework/commands/rest_builder/endpoint/single_model.py @@ -13,9 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from typing import List - -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.base import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import ( RestEndpointBuilder, RestEntityBuilder, ) @@ -48,7 +48,7 @@ class SingleModelEndpointBuilder(RestEndpointBuilder): SingleModel, ) from splunktaucclib.rest_handler import admin_external, util -from {handler_module} import {handler_name} +from {handler_module} import {handler_class} import logging util.remove_http_proxy_env_vars() @@ -66,18 +66,18 @@ class SingleModelEndpointBuilder(RestEndpointBuilder): logging.getLogger().addHandler(logging.NullHandler()) admin_external.handle( endpoint, - handler={handler_name}, + handler={handler_class}, ) """ - def actions(self): + def actions(self) -> List[str]: return ["edit", "list", "remove", "create"] - def generate_rh(self, handler): + def generate_rh(self) -> str: entity = self._entities[0] return self._rh_template.format( - handler_module=handler.module, - handler_name=handler.name, + handler_module=self.rh_module, + handler_class=self.rh_class, entity=entity.generate_rh(), conf_name=self.conf_name, config_name=self._name, diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/global_config.py b/splunk_add_on_ucc_framework/commands/rest_builder/global_config.py similarity index 61% rename from splunk_add_on_ucc_framework/uccrestbuilder/global_config.py rename to splunk_add_on_ucc_framework/commands/rest_builder/global_config.py index ed9c3d1ba..adf44c499 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/global_config.py +++ b/splunk_add_on_ucc_framework/commands/rest_builder/global_config.py @@ -23,24 +23,35 @@ import os import os.path as op import shutil +from typing import Any, Dict, List, Type -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.base import indent, quote_regex -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.datainput import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import ( + RestEndpointBuilder, +) +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.datainput import ( DataInputEndpointBuilder, DataInputEntityBuilder, ) -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.field import RestFieldBuilder -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.multiple_model import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.field import ( + RestFieldBuilder, +) +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.multiple_model import ( MultipleModelEndpointBuilder, MultipleModelEntityBuilder, ) -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.oauth_model import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.oauth_model import ( OAuthModelEndpointBuilder, ) -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.single_model import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.single_model import ( SingleModelEndpointBuilder, SingleModelEntityBuilder, ) +from splunk_add_on_ucc_framework.commands.rest_builder.validator_builder import ( + ValidatorBuilder, +) + +REST_HANDLER_DEFAULT_MODULE = "splunktaucclib.rest_handler.admin_external" +REST_HANDLER_DEFAULT_CLASS = "AdminExternalHandler" def _is_true(val): @@ -54,16 +65,16 @@ def __init__(self, content, j2_env): self._configs = [] self._settings = [] self.j2_env = j2_env - self._endpoints = {} + self._endpoints: Dict[str, RestEndpointBuilder] = {} self._parse() self._parse_builder_schema() @property - def product(self): + def product(self) -> str: return self._meta["name"] @property - def namespace(self): + def namespace(self) -> str: return self._meta["restRoot"] @property @@ -83,8 +94,8 @@ def settings(self): return self._settings @property - def endpoints(self): - return [endpoint for _, endpoint in list(self._endpoints.items())] + def endpoints(self) -> List[RestEndpointBuilder]: + return list(self._endpoints.values()) def _parse(self): self._meta = self._content["meta"] @@ -112,18 +123,23 @@ def _parse_builder_schema(self): self._builder_inputs() def _builder_configs(self): - # SingleModel for config in self._configs: - self._builder_entity( - None, - config["entity"], + endpoint_obj = self._get_endpoint( config["name"], SingleModelEndpointBuilder, - SingleModelEntityBuilder, - conf_name=config.get("conf"), rest_handler_name=config.get("restHandlerName"), + rest_handler_module=REST_HANDLER_DEFAULT_MODULE, + rest_handler_class=REST_HANDLER_DEFAULT_CLASS, ) - # If we have have given oauth support then we have to add endpoint for accesstoken + content = self._get_oauth_enitities(config["entity"]) + fields = self._parse_fields(content) + entity = SingleModelEntityBuilder( + None, + fields, + conf_name=config.get("conf"), + ) + endpoint_obj.add_entity(entity) + # If we have given oauth support then we have to add endpoint for accesstoken for entity_element in config["entity"]: if entity_element["type"] == "oauth": self._get_endpoint( @@ -131,53 +147,65 @@ def _builder_configs(self): ) def _builder_settings(self): - # MultipleModel for setting in self._settings: - self._builder_entity( - setting["name"], - setting["entity"], + endpoint_obj = self._get_endpoint( "settings", MultipleModelEndpointBuilder, - MultipleModelEntityBuilder, + rest_handler_module=REST_HANDLER_DEFAULT_MODULE, + rest_handler_class=REST_HANDLER_DEFAULT_CLASS, ) + content = self._get_oauth_enitities(setting["entity"]) + fields = self._parse_fields(content) + entity = MultipleModelEntityBuilder( + setting["name"], + fields, + ) + endpoint_obj.add_entity(entity) def _builder_inputs(self): - # DataInput for input_item in self._inputs: - rest_handler_name = None - if "restHandlerName" in input_item: - rest_handler_name = input_item["restHandlerName"] + rest_handler_name = input_item.get("restHandlerName") + rest_handler_module = input_item.get( + "restHandlerModule", + REST_HANDLER_DEFAULT_MODULE, + ) + rest_handler_class = input_item.get( + "restHandlerClass", + REST_HANDLER_DEFAULT_CLASS, + ) if "conf" in input_item: - self._builder_entity( - None, - input_item["entity"], + endpoint_obj = self._get_endpoint( input_item["name"], SingleModelEndpointBuilder, - SingleModelEntityBuilder, - conf_name=input_item["conf"], rest_handler_name=rest_handler_name, + rest_handler_module=rest_handler_module, + rest_handler_class=rest_handler_class, ) - else: - self._builder_entity( + content = self._get_oauth_enitities(input_item["entity"]) + fields = self._parse_fields(content) + entity = SingleModelEntityBuilder( None, - input_item["entity"], + fields, + conf_name=input_item["conf"], + ) + endpoint_obj.add_entity(entity) + else: + endpoint_obj = self._get_endpoint( input_item["name"], DataInputEndpointBuilder, - DataInputEntityBuilder, input_type=input_item["name"], rest_handler_name=rest_handler_name, + rest_handler_module=rest_handler_module, + rest_handler_class=rest_handler_class, ) - - def _builder_entity( - self, name, content, endpoint, endpoint_builder, entity_builder, *args, **kwargs - ): - endpoint_obj = self._get_endpoint(endpoint, endpoint_builder, *args, **kwargs) - # If the entity contains type oauth then we need to alter the content to generate proper entities to generate - # the rest handler with the oauth fields - content = self._get_oauth_enitities(content) - fields = self._parse_fields(content) - entity = entity_builder(name, fields, *args, **kwargs) - endpoint_obj.add_entity(entity) + content = self._get_oauth_enitities(input_item["entity"]) + fields = self._parse_fields(content) + entity = DataInputEntityBuilder( + None, + fields, + input_type=input_item["name"], + ) + endpoint_obj.add_entity(entity) def _parse_fields(self, fields_content): return [ @@ -186,31 +214,28 @@ def _parse_fields(self, fields_content): if field["field"] != "name" ] - def _get_endpoint(self, name, endpoint_builder, *args, **kwargs): + def _get_endpoint( + self, name: str, endpoint_builder: Type[RestEndpointBuilder], **kwargs: Any + ): if name not in self._endpoints: endpoint = endpoint_builder( name=name, namespace=self._meta["restRoot"], j2_env=self.j2_env, - *args, **kwargs, ) self._endpoints[name] = endpoint return self._endpoints[name] - def _parse_field(self, content): + def _parse_field(self, content) -> RestFieldBuilder: return RestFieldBuilder( content["field"], _is_true(content.get("required")), _is_true(content.get("encrypted")), content.get("defaultValue"), - self._parse_validation(content.get("validators")), + ValidatorBuilder().build(content.get("validators")), ) - def _parse_validation(self, validation): - global_config_validation = GlobalConfigValidation(validation) - return global_config_validation.build() - """ If the entity contains type oauth then we need to alter the content to generate proper entities to generate the rest handler with the oauth fields @@ -254,129 +279,6 @@ def _get_oauth_enitities(self, content): return content -class GlobalConfigValidation: - - _validation_template = """validator.{validator}({arguments})""" - - def __init__(self, validation): - self._validators = [] - self._validation = validation - self._validation_mapping = { - "string": GlobalConfigValidation.string, - "number": GlobalConfigValidation.number, - "regex": GlobalConfigValidation.regex, - "email": GlobalConfigValidation.email, - "ipv4": GlobalConfigValidation.ipv4, - "date": GlobalConfigValidation.date, - "url": GlobalConfigValidation.url, - } - - def build(self): - if not self._validation: - return None - for item in self._validation: - parser = self._validation_mapping.get(item["type"], None) - if parser is None: - continue - validator, arguments = parser(item) - if validator is None: - continue - arguments = arguments or {} - self._validators.append( - self._validation_template.format( - validator=validator, - arguments=self._arguments(**arguments), - ) - ) - - if not self._validators: - return None - if len(self._validators) > 1: - return self.multiple_validators(self._validators) - else: - return self._validators[0] - - @classmethod - def _arguments(cls, **kwargs): - if not kwargs: - return "" - args = list( - map( - lambda k_v: f"{k_v[0]}={k_v[1]}, ", - list(kwargs.items()), - ) - ) - args.insert(0, "") - args.append("") - return indent("\n".join(args)) - - @classmethod - def _content(cls, validator, arguments): - pass - - @classmethod - def string(cls, validation): - return ( - "String", - { - "max_len": validation.get("maxLength"), - "min_len": validation.get("minLength"), - }, - ) - - @classmethod - def number(cls, validation): - ranges = validation.get("range", [None, None]) - return ("Number", {"max_val": ranges[1], "min_val": ranges[0]}) - - @classmethod - def regex(cls, validation): - return ("Pattern", {"regex": "r" + quote_regex(validation.get("pattern"))}) - - @classmethod - def email(cls, validation): - regex = ( - r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}" - r"[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" - ) - return ("Pattern", {"regex": "r" + quote_regex(regex)}) - - @classmethod - def ipv4(cls, validation): - regex = r"^(?:(?:[0-1]?\d{1,2}|2[0-4]\d|25[0-5])(?:\.|$)){4}$" - return ("Pattern", {"regex": "r" + quote_regex(regex)}) - - @classmethod - def date(cls, validation): - # iso8601 date time format - regex = ( - r"^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))" - r"(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$" - ) - return ("Pattern", {"regex": "r" + quote_regex(regex)}) - - @classmethod - def url(cls, validation): - regex = ( - r"^(?:(?:https?|ftp|opc\.tcp):\/\/)?(?:\S+(?::\S*)?@)?" - r"(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" - r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}" - r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|" - r"(?:(?:[a-z\u00a1-\uffff0-9]+-?_?)*[a-z\u00a1-\uffff0-9]+)" - r"(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*" - r"(?:\.(?:[a-z\u00a1-\uffff]{2,}))?)(?::\d{2,5})?(?:\/[^\s]*)?$" - ) - return ("Pattern", {"regex": "r" + quote_regex(regex)}) - - @classmethod - def multiple_validators(cls, validators): - validators_str = ", \n".join(validators) - _template = """validator.AllOf(\n{validators}\n)""" - return _template.format( - validators=indent(validators_str), - ) - - class GlobalConfigPostProcessor: """ Post process for REST builder. diff --git a/splunk_add_on_ucc_framework/commands/rest_builder/validator_builder.py b/splunk_add_on_ucc_framework/commands/rest_builder/validator_builder.py new file mode 100644 index 000000000..d702e6581 --- /dev/null +++ b/splunk_add_on_ucc_framework/commands/rest_builder/validator_builder.py @@ -0,0 +1,170 @@ +# +# Copyright 2021 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# +from typing import Any, Dict, Optional, Sequence + +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import ( + indent, + quote_regex, +) + + +class BaseValidator: + _validation_template = """validator.{class_name}({arguments})""" + + def _get_class_name(self) -> str: + raise NotImplementedError() + + def _get_arguments(self, config: Dict[str, Any]) -> Dict[str, Any]: + raise NotImplementedError() + + def _format_arguments(self, **kwargs: Dict[str, Any]) -> str: + if not kwargs: + return "" + args = list( + map( + lambda k_v: f"{k_v[0]}={k_v[1]}, ", + list(kwargs.items()), + ) + ) + args.insert(0, "") + args.append("") + return indent("\n".join(args)) + + def build(self, config: Dict[str, Any]) -> str: + return self._validation_template.format( + class_name=self._get_class_name(), + arguments=self._format_arguments(**self._get_arguments(config)), + ) + + +class StringValidator(BaseValidator): + def _get_class_name(self) -> str: + return "String" + + def _get_arguments(self, config: Dict[str, Any]) -> Dict[str, Any]: + return { + "max_len": config.get("maxLength"), + "min_len": config.get("minLength"), + } + + +class NumberValidator(BaseValidator): + def _get_class_name(self) -> str: + return "Number" + + def _get_arguments(self, config: Dict[str, Any]) -> Dict[str, Any]: + ranges = config.get("range", [None, None]) + return { + "max_val": ranges[1], + "min_val": ranges[0], + } + + +class RegexValidator(BaseValidator): + def _get_class_name(self) -> str: + return "Pattern" + + def _get_arguments(self, config: Dict[str, Any]) -> Dict[str, Any]: + return {"regex": "r" + quote_regex(config.get("pattern"))} + + +class EmailValidator(BaseValidator): + def _get_class_name(self) -> str: + return "Pattern" + + def _get_arguments(self, config: Dict[str, Any]) -> Dict[str, Any]: + regex = ( + r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}" + r"[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" + ) + return {"regex": "r" + quote_regex(regex)} + + +class Ipv4Validator(BaseValidator): + def _get_class_name(self) -> str: + return "Pattern" + + def _get_arguments(self, config: Dict[str, Any]) -> Dict[str, Any]: + regex = r"^(?:(?:[0-1]?\d{1,2}|2[0-4]\d|25[0-5])(?:\.|$)){4}$" + return {"regex": "r" + quote_regex(regex)} + + +class DateValidator(BaseValidator): + def _get_class_name(self) -> str: + return "Pattern" + + def _get_arguments(self, config: Dict[str, Any]) -> Dict[str, Any]: + # iso8601 date time format + regex = ( + r"^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))" + r"(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$" + ) + return {"regex": "r" + quote_regex(regex)} + + +class UrlValidator(BaseValidator): + def _get_class_name(self) -> str: + return "Pattern" + + def _get_arguments(self, config: Dict[str, Any]) -> Dict[str, Any]: + regex = ( + r"^(?:(?:https?|ftp|opc\.tcp):\/\/)?(?:\S+(?::\S*)?@)?" + r"(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" + r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}" + r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|" + r"(?:(?:[a-z\u00a1-\uffff0-9]+-?_?)*[a-z\u00a1-\uffff0-9]+)" + r"(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*" + r"(?:\.(?:[a-z\u00a1-\uffff]{2,}))?)(?::\d{2,5})?(?:\/[^\s]*)?$" + ) + return {"regex": "r" + quote_regex(regex)} + + +class ValidatorBuilder: + _validation_config_map = { + "string": StringValidator, + "number": NumberValidator, + "regex": RegexValidator, + "email": EmailValidator, + "ipv4": Ipv4Validator, + "date": DateValidator, + "url": UrlValidator, + # file validator does not need any generated code, everything is + # validated in the UI + } + + def _format_multiple_validators(self, validators: Sequence[str]) -> str: + validators_str = ", \n".join(validators) + return """validator.AllOf(\n{validators}\n)""".format( + validators=indent(validators_str), + ) + + def build(self, configs: Optional[Sequence[Dict[str, Any]]]) -> Optional[str]: + if configs is None: + return None + generated_validators = [] + for config in configs: + config_type = config.get("type") + if config_type is None: + continue + validator = self._validation_config_map.get(config_type) + if validator is None: + continue + generated_validators.append(validator().build(config)) + if not generated_validators: + return None + if len(generated_validators) > 1: + return self._format_multiple_validators(generated_validators) + return generated_validators[0] diff --git a/splunk_add_on_ucc_framework/global_config_validator.py b/splunk_add_on_ucc_framework/global_config_validator.py index a453fa14e..5e2ee190f 100644 --- a/splunk_add_on_ucc_framework/global_config_validator.py +++ b/splunk_add_on_ucc_framework/global_config_validator.py @@ -71,6 +71,76 @@ def _validate_configuration_tab_table_has_name_field(self) -> None: f"Tab '{tab['name']}' should have entity with field 'name'" ) + def _validate_custom_rest_handlers(self) -> None: + """ + Validates that only "restHandlerName" or both "restHandlerModule" and + "restHandlerClass" is present in the input configuration. Also validates + that both "restHandlerModule" and "restHandlerClass" is present if any + of them are present. + + The valid scenarios: + * only restHandlerName is present + * both restHandlerModule and restHandlerClass is present + Everything other combination is considered invalid. + """ + pages = self._config["pages"] + inputs = pages.get("inputs") + if inputs is None: + return + services = inputs["services"] + for service in services: + rest_handler_name = service.get("restHandlerName") + rest_handler_module = service.get("restHandlerModule") + rest_handler_class = service.get("restHandlerClass") + if rest_handler_name is not None and ( + rest_handler_module is not None or rest_handler_class is not None + ): + raise GlobalConfigValidatorException( + f"Input '{service['name']}' has both 'restHandlerName' and " + f"'restHandlerModule' or 'restHandlerClass' fields present. " + f"Please use only 'restHandlerName' or 'restHandlerModule' " + f"and 'restHandlerClass'." + ) + if (rest_handler_module is not None and rest_handler_class is None) or ( + rest_handler_module is None and rest_handler_class is not None + ): + raise GlobalConfigValidatorException( + f"Input '{service['name']}' should have both " + f"'restHandlerModule' and 'restHandlerClass' fields " + f"present, only 1 of them was found." + ) + + def _validate_file_input_configuration(self) -> None: + pages = self._config["pages"] + configuration = pages["configuration"] + tabs = configuration["tabs"] + for tab in tabs: + entities = tab["entity"] + for entity in entities: + if entity["type"] == "file": + validators = entity.get("validators") + if validators is None: + raise GlobalConfigValidatorException( + f"File validator should be present for " + f"'{entity['field']}' field." + ) + for validator in validators: + if validator.get("type") == "file": + supported_file_types = validator.get("supportedFileTypes") + if supported_file_types is None: + raise GlobalConfigValidatorException( + f"`json` should be present in the " + f"'supportedFileTypes' for " + f"'{entity['field']}' field." + ) + if supported_file_types[0] != "json": + raise GlobalConfigValidatorException( + f"`json` is only currently supported for " + f"file input for '{entity['field']}' field." + ) + def validate(self) -> None: self._validate_config_against_schema() self._validate_configuration_tab_table_has_name_field() + self._validate_custom_rest_handlers() + self._validate_file_input_configuration() diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/rest_conf.py b/splunk_add_on_ucc_framework/rest_map_conf.py similarity index 68% rename from splunk_add_on_ucc_framework/uccrestbuilder/rest_conf.py rename to splunk_add_on_ucc_framework/rest_map_conf.py index 10f6051de..d94dc550f 100644 --- a/splunk_add_on_ucc_framework/uccrestbuilder/rest_conf.py +++ b/splunk_add_on_ucc_framework/rest_map_conf.py @@ -15,7 +15,9 @@ # from typing import Sequence -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.base import RestEndpointBuilder +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import ( + RestEndpointBuilder, +) class RestmapConf: @@ -61,36 +63,3 @@ def build( @classmethod def admin_externals(cls, endpoints): return [endpoint.name for endpoint in endpoints] - - -class WebConf: - - _template = """ -[expose:{name}] -pattern = {name} -methods = POST, GET -""" - - _specified_template = """ -[expose:{name}_specified] -pattern = {name}/* -methods = POST, GET, DELETE -""" - - @classmethod - def build(cls, endpoints: Sequence[RestEndpointBuilder]) -> str: - stanzas = [] - for endpoint in endpoints: - stanzas.append( - cls._template.format( - namespace=endpoint.namespace, - name=endpoint.name, - ) - ) - stanzas.append( - cls._specified_template.format( - namespace=endpoint.namespace, - name=endpoint.name, - ) - ) - return "".join(stanzas) diff --git a/splunk_add_on_ucc_framework/uccrestbuilder/__init__.py b/splunk_add_on_ucc_framework/uccrestbuilder/__init__.py deleted file mode 100644 index 9a1f7c4d2..000000000 --- a/splunk_add_on_ucc_framework/uccrestbuilder/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -# -# Copyright 2021 Splunk Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# 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. -# - -""" -REST Builder. -""" -import collections - -from splunk_add_on_ucc_framework.uccrestbuilder.builder import ( - RestBuilder, - RestBuilderError, -) - -__all__ = [ - "RestBuilder", - "RestBuilderError", - "RestHandlerClass", - "build", -] - -RestHandlerClass = collections.namedtuple( - "RestHandlerClass", - ("module", "name"), -) - - -def build( - schema, - rest_handler_module, - rest_handler_class, - output_path, - post_process=None, - *args, - **kwargs -): - builder_obj = RestBuilder( - schema, RestHandlerClass(rest_handler_module, rest_handler_class), output_path - ) - builder_obj.build() - if post_process is not None: - post_process(builder_obj, schema, *args, **kwargs) - return builder_obj diff --git a/splunk_add_on_ucc_framework/web_conf.py b/splunk_add_on_ucc_framework/web_conf.py new file mode 100644 index 000000000..a71d91435 --- /dev/null +++ b/splunk_add_on_ucc_framework/web_conf.py @@ -0,0 +1,53 @@ +# +# Copyright 2021 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# +from typing import Sequence + +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import ( + RestEndpointBuilder, +) + + +class WebConf: + + _template = """ +[expose:{name}] +pattern = {name} +methods = POST, GET +""" + + _specified_template = """ +[expose:{name}_specified] +pattern = {name}/* +methods = POST, GET, DELETE +""" + + @classmethod + def build(cls, endpoints: Sequence[RestEndpointBuilder]) -> str: + stanzas = [] + for endpoint in endpoints: + stanzas.append( + cls._template.format( + namespace=endpoint.namespace, + name=endpoint.name, + ) + ) + stanzas.append( + cls._specified_template.format( + namespace=endpoint.namespace, + name=endpoint.name, + ) + ) + return "".join(stanzas) diff --git a/tests/smoke/test_ucc_generate.py b/tests/smoke/test_ucc_generate.py index ede09bd61..e1762483b 100644 --- a/tests/smoke/test_ucc_generate.py +++ b/tests/smoke/test_ucc_generate.py @@ -1,8 +1,8 @@ +import difflib import tempfile from os import path import splunk_add_on_ucc_framework as ucc -from tests.unit.helpers import assert_identical_files def test_ucc_generate(): @@ -84,11 +84,14 @@ def test_ucc_generate_with_inputs_configuration_alerts(): ("bin", "example_input_one.py"), ("bin", "example_input_two.py"), ("bin", "example_input_three.py"), + ("bin", "example_input_four.py"), ("bin", "import_declare_test.py"), ("bin", "splunk_ta_uccexample_rh_account.py"), ("bin", "splunk_ta_uccexample_rh_example_input_one.py"), ("bin", "splunk_ta_uccexample_rh_example_input_two.py"), ("bin", "splunk_ta_uccexample_rh_three_custom.py"), + ("bin", "splunk_ta_uccexample_rh_example_input_four.py"), + ("bin", "splunk_ta_uccexample_custom_rh.py"), ("bin", "splunk_ta_uccexample_rh_oauth.py"), ("bin", "splunk_ta_uccexample_rh_settings.py"), ("bin", "test_alert.py"), @@ -98,13 +101,26 @@ def test_ucc_generate_with_inputs_configuration_alerts(): ("README", "splunk_ta_uccexample_settings.conf.spec"), ("metadata", "default.meta"), ] + diff_results = [] for f in files_to_be_equal: expected_file_path = path.join(expected_folder, *f) actual_file_path = path.join(actual_folder, *f) - assert assert_identical_files( - expected_file_path, - actual_file_path, - ), f"Expected file {expected_file_path} is different from {actual_file_path}" + with open(expected_file_path) as expected_file: + expected_file_lines = expected_file.readlines() + with open(actual_file_path) as actual_file: + actual_file_lines = actual_file.readlines() + for line in difflib.unified_diff( + actual_file_lines, + expected_file_lines, + fromfile=actual_file_path, + tofile=expected_file_path, + lineterm="", + ): + diff_results.append(line) + if diff_results: + for result in diff_results: + print(result) + assert False, "Some diffs were found" files_to_exist = [ ("static", "appIcon.png"), ("static", "appIcon_2x.png"), @@ -164,13 +180,26 @@ def test_ucc_generate_with_configuration(): ("README", "splunk_ta_uccexample_settings.conf.spec"), ("metadata", "default.meta"), ] + diff_results = [] for f in files_to_be_equal: expected_file_path = path.join(expected_folder, *f) actual_file_path = path.join(actual_folder, *f) - assert assert_identical_files( - expected_file_path, - actual_file_path, - ), f"Expected file {expected_file_path} is different from {actual_file_path}" + with open(expected_file_path) as expected_file: + expected_file_lines = expected_file.readlines() + with open(actual_file_path) as actual_file: + actual_file_lines = actual_file.readlines() + for line in difflib.unified_diff( + actual_file_lines, + expected_file_lines, + fromfile=actual_file_path, + tofile=expected_file_path, + lineterm="", + ): + diff_results.append(line) + if diff_results: + for result in diff_results: + print(result) + assert False, "Some diffs were found" files_to_exist = [ ("static", "appIcon.png"), ("static", "appIcon_2x.png"), @@ -221,13 +250,26 @@ def test_ucc_generate_with_configuration_files_only(): ("default", "tags.conf"), ("metadata", "default.meta"), ] + diff_results = [] for f in files_to_be_equal: expected_file_path = path.join(expected_folder, *f) actual_file_path = path.join(actual_folder, *f) - assert assert_identical_files( - expected_file_path, - actual_file_path, - ), f"Expected file {expected_file_path} is different from {actual_file_path}" + with open(expected_file_path) as expected_file: + expected_file_lines = expected_file.readlines() + with open(actual_file_path) as actual_file: + actual_file_lines = actual_file.readlines() + for line in difflib.unified_diff( + actual_file_lines, + expected_file_lines, + fromfile=actual_file_path, + tofile=expected_file_path, + lineterm="", + ): + diff_results.append(line) + if diff_results: + for result in diff_results: + print(result) + assert False, "Some diffs were found" files_to_not_exist = [ ("default", "data", "ui", "nav", "default_no_input.xml"), ] diff --git a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/README/inputs.conf.spec b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/README/inputs.conf.spec index 40c4e629b..43666b767 100644 --- a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/README/inputs.conf.spec +++ b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/README/inputs.conf.spec @@ -26,4 +26,7 @@ start_date = example_help_link = [example_input_three://] +interval = + +[example_input_four://] interval = \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/example_input_four.py b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/example_input_four.py new file mode 100644 index 000000000..d97d452f5 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/example_input_four.py @@ -0,0 +1,54 @@ +import import_declare_test +import sys +import json + +from splunklib import modularinput as smi + +class EXAMPLE_INPUT_FOUR(smi.Script): + + def __init__(self): + super(EXAMPLE_INPUT_FOUR, self).__init__() + + def get_scheme(self): + scheme = smi.Scheme('example_input_four') + scheme.description = 'Example Input Four' + scheme.use_external_validation = True + scheme.streaming_mode_xml = True + scheme.use_single_instance = True + + scheme.add_argument( + smi.Argument( + 'name', + title='Name', + description='Name', + required_on_create=True + ) + ) + + scheme.add_argument( + smi.Argument( + 'interval', + required_on_create=True, + ) + ) + + return scheme + + def validate_input(self, definition): + return + + def stream_events(self, inputs, ew): + input_items = [{'count': len(inputs.inputs)}] + for input_name, input_item in inputs.inputs.items(): + input_item['name'] = input_name + input_items.append(input_item) + event = smi.Event( + data=json.dumps(input_items), + sourcetype='example_input_four', + ) + ew.write_event(event) + + +if __name__ == '__main__': + exit_code = EXAMPLE_INPUT_FOUR().run(sys.argv) + sys.exit(exit_code) \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/splunk_ta_uccexample_custom_rh.py b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/splunk_ta_uccexample_custom_rh.py new file mode 100644 index 000000000..77a709101 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/splunk_ta_uccexample_custom_rh.py @@ -0,0 +1,20 @@ +import import_declare_test + +from splunktaucclib.rest_handler.admin_external import AdminExternalHandler + + +class CustomRestHandler(AdminExternalHandler): + def __init__(self, *args, **kwargs): + AdminExternalHandler.__init__(self, *args, **kwargs) + + def handleList(self, confInfo): + AdminExternalHandler.handleList(self, confInfo) + + def handleEdit(self, confInfo): + AdminExternalHandler.handleEdit(self, confInfo) + + def handleCreate(self, confInfo): + AdminExternalHandler.handleCreate(self, confInfo) + + def handleRemove(self, confInfo): + AdminExternalHandler.handleRemove(self, confInfo) diff --git a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/splunk_ta_uccexample_rh_example_input_four.py b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/splunk_ta_uccexample_rh_example_input_four.py new file mode 100644 index 000000000..7d7e54b69 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/bin/splunk_ta_uccexample_rh_example_input_four.py @@ -0,0 +1,50 @@ + +import import_declare_test + +from splunktaucclib.rest_handler.endpoint import ( + field, + validator, + RestModel, + DataInputModel, +) +from splunktaucclib.rest_handler import admin_external, util +from splunk_ta_uccexample_custom_rh import CustomRestHandler +import logging + +util.remove_http_proxy_env_vars() + + +fields = [ + field.RestField( + 'interval', + required=True, + encrypted=False, + default=None, + validator=validator.Pattern( + regex=r"""^\-[1-9]\d*$|^\d*$""", + ) + ), + + field.RestField( + 'disabled', + required=False, + validator=None + ) + +] +model = RestModel(fields, name=None) + + + +endpoint = DataInputModel( + 'example_input_four', + model, +) + + +if __name__ == '__main__': + logging.getLogger().addHandler(logging.NullHandler()) + admin_external.handle( + endpoint, + handler=CustomRestHandler, + ) diff --git a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/inputs.conf b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/inputs.conf index de457355f..539b308b0 100644 --- a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/inputs.conf +++ b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/inputs.conf @@ -7,3 +7,6 @@ python.version = python3 [example_input_three] python.version = python3 +[example_input_four] +python.version = python3 + diff --git a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/restmap.conf b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/restmap.conf index 33310dcb3..8286ae611 100644 --- a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/restmap.conf +++ b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/restmap.conf @@ -1,7 +1,7 @@ [admin:splunk_ta_uccexample] match = / -members = splunk_ta_uccexample_account, splunk_ta_uccexample_oauth, splunk_ta_uccexample_settings, splunk_ta_uccexample_example_input_one, splunk_ta_uccexample_example_input_two, splunk_ta_uccexample_example_input_three +members = splunk_ta_uccexample_account, splunk_ta_uccexample_oauth, splunk_ta_uccexample_settings, splunk_ta_uccexample_example_input_one, splunk_ta_uccexample_example_input_two, splunk_ta_uccexample_example_input_three, splunk_ta_uccexample_example_input_four [admin_external:splunk_ta_uccexample_account] handlertype = python @@ -44,3 +44,10 @@ python.version = python3 handlerfile = splunk_ta_uccexample_rh_three_custom.py handleractions = edit, list, remove, create handlerpersistentmode = true + +[admin_external:splunk_ta_uccexample_example_input_four] +handlertype = python +python.version = python3 +handlerfile = splunk_ta_uccexample_rh_example_input_four.py +handleractions = edit, list, remove, create +handlerpersistentmode = true diff --git a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/web.conf b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/web.conf index 4eb91acf1..014338b16 100644 --- a/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/web.conf +++ b/tests/testdata/expected_addons/expected_output_global_config_inputs_configuration_alerts/Splunk_TA_UCCExample/default/web.conf @@ -46,3 +46,11 @@ methods = POST, GET [expose:splunk_ta_uccexample_example_input_three_specified] pattern = splunk_ta_uccexample_example_input_three/* methods = POST, GET, DELETE + +[expose:splunk_ta_uccexample_example_input_four] +pattern = splunk_ta_uccexample_example_input_four +methods = POST, GET + +[expose:splunk_ta_uccexample_example_input_four_specified] +pattern = splunk_ta_uccexample_example_input_four/* +methods = POST, GET, DELETE diff --git a/tests/testdata/test_addons/package_global_config_configuration/globalConfig.json b/tests/testdata/test_addons/package_global_config_configuration/globalConfig.json index 552b492a0..cc862ba2b 100644 --- a/tests/testdata/test_addons/package_global_config_configuration/globalConfig.json +++ b/tests/testdata/test_addons/package_global_config_configuration/globalConfig.json @@ -421,7 +421,7 @@ "meta": { "name": "Splunk_TA_UCCExample", "restRoot": "splunk_ta_uccexample", - "version": "5.15.1Rd4868aeb", + "version": "5.17.1Re5feae2f", "displayName": "Splunk UCC test Add-on", "schemaVersion": "0.0.3" } diff --git a/tests/testdata/test_addons/package_global_config_inputs_configuration_alerts/globalConfig.json b/tests/testdata/test_addons/package_global_config_inputs_configuration_alerts/globalConfig.json index ff692c888..9ec1b8aef 100644 --- a/tests/testdata/test_addons/package_global_config_inputs_configuration_alerts/globalConfig.json +++ b/tests/testdata/test_addons/package_global_config_inputs_configuration_alerts/globalConfig.json @@ -881,6 +881,48 @@ } ], "title": "Example Input Three" + }, + { + "name": "example_input_four", + "restHandlerModule": "splunk_ta_uccexample_custom_rh", + "restHandlerClass": "CustomRestHandler", + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds.", + "required": true + } + ], + "title": "Example Input Four" } ], "title": "Inputs", @@ -1066,7 +1108,7 @@ "meta": { "name": "Splunk_TA_UCCExample", "restRoot": "splunk_ta_uccexample", - "version": "5.15.1Rd4868aeb", + "version": "5.17.1Re5feae2f", "displayName": "Splunk UCC test Add-on", "schemaVersion": "0.0.3" } diff --git a/tests/testdata/test_addons/package_global_config_inputs_configuration_alerts/package/bin/splunk_ta_uccexample_custom_rh.py b/tests/testdata/test_addons/package_global_config_inputs_configuration_alerts/package/bin/splunk_ta_uccexample_custom_rh.py new file mode 100644 index 000000000..77a709101 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_inputs_configuration_alerts/package/bin/splunk_ta_uccexample_custom_rh.py @@ -0,0 +1,20 @@ +import import_declare_test + +from splunktaucclib.rest_handler.admin_external import AdminExternalHandler + + +class CustomRestHandler(AdminExternalHandler): + def __init__(self, *args, **kwargs): + AdminExternalHandler.__init__(self, *args, **kwargs) + + def handleList(self, confInfo): + AdminExternalHandler.handleList(self, confInfo) + + def handleEdit(self, confInfo): + AdminExternalHandler.handleEdit(self, confInfo) + + def handleCreate(self, confInfo): + AdminExternalHandler.handleCreate(self, confInfo) + + def handleRemove(self, confInfo): + AdminExternalHandler.handleRemove(self, confInfo) diff --git a/tests/unit/helpers.py b/tests/unit/helpers.py index 72d3fe866..dee1e9b1b 100644 --- a/tests/unit/helpers.py +++ b/tests/unit/helpers.py @@ -24,14 +24,6 @@ yaml_load = functools.partial(yaml.load, Loader=Loader) -def assert_identical_files(expected_file_path: str, file_path: str) -> bool: - with open(expected_file_path) as expected_fd: - expected_content = expected_fd.read() - with open(file_path) as fd: - content = fd.read() - return expected_content == content - - def get_testdata_file_path(file_name: str) -> str: return os.path.join( os.path.dirname(os.path.realpath(__file__)), "testdata", file_name diff --git a/tests/unit/test_config_validation.py b/tests/unit/test_config_validation.py deleted file mode 100644 index 61701eaac..000000000 --- a/tests/unit/test_config_validation.py +++ /dev/null @@ -1,62 +0,0 @@ -# -# Copyright 2021 Splunk Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# 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. -# -import os -from contextlib import nullcontext as does_not_raise - -import pytest - -import tests.unit.helpers as helpers -from splunk_add_on_ucc_framework.global_config_validator import ( - GlobalConfigValidator, - GlobalConfigValidatorException, -) - - -def _path_to_source_dir() -> str: - return os.path.join( - os.getcwd(), - "splunk_add_on_ucc_framework", - ) - - -@pytest.mark.parametrize( - "filename,expectation", - [ - ("valid_config.json", does_not_raise()), - ( - "invalid_config_no_configuration_tabs.json", - pytest.raises(GlobalConfigValidatorException), - ), - ( - "invalid_config_no_name_field_in_configuration_tab_table.json", - pytest.raises(GlobalConfigValidatorException), - ), - ("valid_config.yaml", does_not_raise()), - ( - "invalid_config_no_configuration_tabs.yaml", - pytest.raises(GlobalConfigValidatorException), - ), - ( - "invalid_config_no_name_field_in_configuration_tab_table.yaml", - pytest.raises(GlobalConfigValidatorException), - ), - ], -) -def test_config_validation(filename, expectation): - config = helpers.get_testdata(filename) - validator = GlobalConfigValidator(_path_to_source_dir(), config) - with expectation: - validator.validate() diff --git a/tests/unit/test_global_config_validator.py b/tests/unit/test_global_config_validator.py new file mode 100644 index 000000000..4e23127ac --- /dev/null +++ b/tests/unit/test_global_config_validator.py @@ -0,0 +1,144 @@ +# +# Copyright 2021 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# +import os +from contextlib import nullcontext as does_not_raise + +import pytest + +import tests.unit.helpers as helpers +from splunk_add_on_ucc_framework.global_config_validator import ( + GlobalConfigValidator, + GlobalConfigValidatorException, +) + + +def _path_to_source_dir() -> str: + return os.path.join( + os.getcwd(), + "splunk_add_on_ucc_framework", + ) + + +@pytest.mark.parametrize( + "filename", + [ + "valid_config.json", + "valid_config.yaml", + ], +) +def test_config_validation_when_valid(filename): + config = helpers.get_testdata(filename) + validator = GlobalConfigValidator(_path_to_source_dir(), config) + with does_not_raise(): + validator.validate() + + +@pytest.mark.parametrize( + "filename,expectation,exception_message", + [ + ( + "invalid_config_no_configuration_tabs.json", + pytest.raises(GlobalConfigValidatorException), + "[] is too short", + ), + ( + "invalid_config_no_name_field_in_configuration_tab_table.json", + pytest.raises(GlobalConfigValidatorException), + "Tab 'account' should have entity with field 'name'", + ), + # restHandlerName and restHandlerModule are present in the + # "example_input_one" input + ( + "invalid_config_both_rest_handler_name_module_are_present.json", + pytest.raises(GlobalConfigValidatorException), + ( + "Input 'example_input_one' has both 'restHandlerName' and " + "'restHandlerModule' or 'restHandlerClass' fields present. " + "Please use only 'restHandlerName' or 'restHandlerModule' " + "and 'restHandlerClass'." + ), + ), + # restHandlerName and restHandlerClass are present in the + # "example_input_one" input + ( + "invalid_config_both_rest_handler_name_class_are_present.json", + pytest.raises(GlobalConfigValidatorException), + ( + "Input 'example_input_one' has both 'restHandlerName' and " + "'restHandlerModule' or 'restHandlerClass' fields present. " + "Please use only 'restHandlerName' or 'restHandlerModule' " + "and 'restHandlerClass'." + ), + ), + # Only restHandlerModule is present in the "example_input_one" input + ( + "invalid_config_only_rest_handler_module_is_present.json", + pytest.raises(GlobalConfigValidatorException), + ( + "Input 'example_input_one' should have both 'restHandlerModule'" + " and 'restHandlerClass' fields present, only 1 of them was found." + ), + ), + # Only restHandlerClass is present in the "example_input_one" input + ( + "invalid_config_only_rest_handler_class_is_present.json", + pytest.raises(GlobalConfigValidatorException), + ( + "Input 'example_input_one' should have both 'restHandlerModule'" + " and 'restHandlerClass' fields present, only 1 of them was found." + ), + ), + ( + "invalid_config_validators_missing_for_file_input.json", + pytest.raises(GlobalConfigValidatorException), + ("File validator should be present for " "'service_account' field."), + ), + ( + "invalid_config_supported_file_types_field_is_missing.json", + pytest.raises(GlobalConfigValidatorException), + ( + "`json` should be present in the " + "'supportedFileTypes' for " + "'service_account' field." + ), + ), + ( + "invalid_config_json_is_missing_in_supported_file_types.json", + pytest.raises(GlobalConfigValidatorException), + ( + "`json` is only currently supported for " + "file input for 'service_account' field." + ), + ), + ( + "invalid_config_no_configuration_tabs.yaml", + pytest.raises(GlobalConfigValidatorException), + "[] is too short", + ), + ( + "invalid_config_no_name_field_in_configuration_tab_table.yaml", + pytest.raises(GlobalConfigValidatorException), + "Tab 'account' should have entity with field 'name'", + ), + ], +) +def test_config_validation_when_error(filename, expectation, exception_message): + config = helpers.get_testdata(filename) + validator = GlobalConfigValidator(_path_to_source_dir(), config) + with expectation as exc_info: + validator.validate() + (msg,) = exc_info.value.args + assert msg == exception_message diff --git a/tests/unit/test_uccrestbuilder_rest_conf.py b/tests/unit/test_restmap_conf.py similarity index 58% rename from tests/unit/test_uccrestbuilder_rest_conf.py rename to tests/unit/test_restmap_conf.py index 1c08a90d0..65fd5392a 100644 --- a/tests/unit/test_uccrestbuilder_rest_conf.py +++ b/tests/unit/test_restmap_conf.py @@ -13,16 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.datainput import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.datainput import ( DataInputEndpointBuilder, ) -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.multiple_model import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.multiple_model import ( MultipleModelEndpointBuilder, ) -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint.single_model import ( +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.single_model import ( SingleModelEndpointBuilder, ) -from splunk_add_on_ucc_framework.uccrestbuilder.rest_conf import RestmapConf, WebConf +from splunk_add_on_ucc_framework.rest_map_conf import RestmapConf def test_rest_conf_build(): @@ -62,40 +62,3 @@ def test_rest_conf_build(): result = RestmapConf.build(endpoints, "addon_name", "") assert expected_result == result - - -def test_web_conf_build(): - endpoints = [ - SingleModelEndpointBuilder("account", "addon_name"), - MultipleModelEndpointBuilder("settings", "addon_name"), - DataInputEndpointBuilder("input_name", "addon_name", "input_name"), - ] - - expected_result = """ -[expose:addon_name_account] -pattern = addon_name_account -methods = POST, GET - -[expose:addon_name_account_specified] -pattern = addon_name_account/* -methods = POST, GET, DELETE - -[expose:addon_name_settings] -pattern = addon_name_settings -methods = POST, GET - -[expose:addon_name_settings_specified] -pattern = addon_name_settings/* -methods = POST, GET, DELETE - -[expose:addon_name_input_name] -pattern = addon_name_input_name -methods = POST, GET - -[expose:addon_name_input_name_specified] -pattern = addon_name_input_name/* -methods = POST, GET, DELETE -""" - result = WebConf.build(endpoints) - - assert expected_result == result diff --git a/tests/unit/test_uccrestbuilder.py b/tests/unit/test_uccrestbuilder.py index 986462b37..5114648fc 100644 --- a/tests/unit/test_uccrestbuilder.py +++ b/tests/unit/test_uccrestbuilder.py @@ -15,7 +15,7 @@ # import pytest -from splunk_add_on_ucc_framework.uccrestbuilder.endpoint import base +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint import base @pytest.mark.parametrize( diff --git a/tests/unit/test_validator_builder.py b/tests/unit/test_validator_builder.py new file mode 100644 index 000000000..fb69af373 --- /dev/null +++ b/tests/unit/test_validator_builder.py @@ -0,0 +1,130 @@ +# +# Copyright 2021 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# +import pytest + +from splunk_add_on_ucc_framework.commands.rest_builder.validator_builder import ( + ValidatorBuilder, +) +from tests.unit.helpers import get_testdata_file + + +@pytest.mark.parametrize( + "config,expected_result", + [ + (None, None), + ( + [ + { + "type": "unknown_validator", + "unknown_argument": "some_value", + } + ], + None, + ), + ( + [ + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100, + } + ], + get_testdata_file("validator_builder_result_string"), + ), + ( + [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", # noqa: E501 + "pattern": "^[a-zA-Z]\\w*$", + }, + ], + get_testdata_file("validator_builder_result_regex"), + ), + ( + [{"type": "number", "range": [1, 65535]}], + get_testdata_file("validator_builder_result_number"), + ), + ( + [{"errorMsg": "Enter a valid Email Address.", "type": "email"}], + get_testdata_file("validator_builder_result_email"), + ), + ( + [{"type": "ipv4"}], + get_testdata_file("validator_builder_result_ipv4"), + ), + ( + [{"type": "date"}], + get_testdata_file("validator_builder_result_date"), + ), + ( + [{"type": "url"}], + get_testdata_file("validator_builder_result_url"), + ), + ( + [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", # noqa: E501 + "pattern": "^[a-zA-Z]\\w*$", + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100, + }, + ], + get_testdata_file("validator_builder_result_string_regex"), + ), + ( + [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", # noqa: E501 + "pattern": "^[a-zA-Z]\\w*$", + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100, + }, + { + "type": "number", + "range": [1, 65535], + }, + { + "errorMsg": "Enter a valid Email Address.", + "type": "email", + }, + { + "type": "ipv4", + }, + { + "type": "date", + }, + { + "type": "url", + }, + ], + get_testdata_file("validator_builder_result_everything"), + ), + ], +) +def test_validator_builder(config, expected_result): + assert expected_result == ValidatorBuilder().build(config) diff --git a/tests/unit/test_web_conf.py b/tests/unit/test_web_conf.py new file mode 100644 index 000000000..452019fb1 --- /dev/null +++ b/tests/unit/test_web_conf.py @@ -0,0 +1,47 @@ +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.datainput import ( + DataInputEndpointBuilder, +) +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.multiple_model import ( + MultipleModelEndpointBuilder, +) +from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.single_model import ( + SingleModelEndpointBuilder, +) +from splunk_add_on_ucc_framework.web_conf import WebConf + + +def test_web_conf_build(): + endpoints = [ + SingleModelEndpointBuilder("account", "addon_name"), + MultipleModelEndpointBuilder("settings", "addon_name"), + DataInputEndpointBuilder("input_name", "addon_name", "input_name"), + ] + + expected_result = """ +[expose:addon_name_account] +pattern = addon_name_account +methods = POST, GET + +[expose:addon_name_account_specified] +pattern = addon_name_account/* +methods = POST, GET, DELETE + +[expose:addon_name_settings] +pattern = addon_name_settings +methods = POST, GET + +[expose:addon_name_settings_specified] +pattern = addon_name_settings/* +methods = POST, GET, DELETE + +[expose:addon_name_input_name] +pattern = addon_name_input_name +methods = POST, GET + +[expose:addon_name_input_name_specified] +pattern = addon_name_input_name/* +methods = POST, GET, DELETE +""" + result = WebConf.build(endpoints) + + assert expected_result == result diff --git a/tests/unit/testdata/invalid_config_both_rest_handler_name_class_are_present.json b/tests/unit/testdata/invalid_config_both_rest_handler_name_class_are_present.json new file mode 100644 index 000000000..042616d6b --- /dev/null +++ b/tests/unit/testdata/invalid_config_both_rest_handler_name_class_are_present.json @@ -0,0 +1,561 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "account", + "table": { + "actions": [ + "edit", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Auth Type", + "field": "auth_type" + } + ] + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "string", + "errorMsg": "Length of ID should be between 1 and 50", + "minLength": 1, + "maxLength": 50 + }, + { + "type": "regex", + "errorMsg": "Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "options": { + "placeholder": "Required" + }, + "field": "name", + "help": "Enter a unique name for this account.", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Environment", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "login.example.com", + "label": "Value1" + }, + { + "value": "test.example.com", + "label": "Value2" + }, + { + "value": "other", + "label": "Other" + } + ], + "display": true + }, + "help": "", + "field": "custom_endpoint", + "defaultValue": "login.example.com", + "required": true + }, + { + "type": "text", + "label": "Endpoint URL", + "help": "Enter the endpoint URL.", + "field": "endpoint", + "options": { + "display": false + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "account_checkbox", + "help": "This is an example checkbox for the account entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "account_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the account entity", + "required": true, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "account_multiple_select", + "help": "This is an example multipleSelect for account entity", + "required": true, + "options": { + "items": [{ + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "text", + "label": "State", + "help": "This is a boolean field for developers to decide whether state parameter will be passed in the OAuth flow. Value: true|false", + "field": "oauth_state_enabled", + "options": { + "display": false + } + }, + { + "type": "oauth", + "field": "oauth", + "label": "Not used", + "options": { + "auth_type": [ + "basic", + "oauth" + ], + "basic": [ + { + "oauth_field": "username", + "label": "Username", + "help": "Enter the username for this account.", + "field": "username" + }, + { + "oauth_field": "password", + "label": "Password", + "encrypted": true, + "help": "Enter the password for this account.", + "field": "password" + }, + { + "oauth_field": "security_token", + "label": "Security Token", + "encrypted": true, + "help": "Enter the security token.", + "field": "token" + } + ], + "oauth": [ + { + "oauth_field": "client_id", + "label": "Client Id", + "field": "client_id", + "help": "Enter the Client Id for this account." + }, + { + "oauth_field": "client_secret", + "label": "Client Secret", + "field": "client_secret", + "encrypted": true, + "help": "Enter the Client Secret key for this account." + }, + { + "oauth_field": "redirect_url", + "label": "Redirect url", + "field": "redirect_url", + "help": "Copy and paste this URL into your app." + } + ], + "auth_code_endpoint": "/services/oauth2/authorize", + "access_token_endpoint": "/services/oauth2/token", + "oauth_timeout": 30 + } + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Account" + } + ], + "title": "Configuration", + "description": "Set up your add-on" + }, + "inputs": { + "services": [ + { + "hook": { + "src": "Hook" + }, + "name": "example_input_one", + "restHandlerName": "custom_rest_handler_file_rh", + "restHandlerClass": "CustomRestHandlerClass", + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "input_one_checkbox", + "help": "This is an example checkbox for the input one entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "input_one_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the input one entity", + "required": false, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "field": "singleSelectTest", + "label": "Single Select Group Test", + "type": "singleSelect", + "options": { + "createSearchChoice": true, + "autoCompleteFields": [ + { + "label": "Group1", + "children": [ + { + "value": "one", + "label": "One" + }, + { + "value": "two", + "label": "Two" + } + ] + }, + { + "label": "Group2", + "children": [ + { + "value": "three", + "label": "Three" + }, + { + "value": "four", + "label": "Four" + } + ] + } + ] + } + }, + { + "field": "multipleSelectTest", + "label": "Multiple Select Test", + "type": "multipleSelect", + "options": { + "delimiter": "|", + "items": [ + { + "value": "a", + "label": "A" + }, + { + "value": "b", + "label": "B" + } + ] + } + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds.", + "required": true + }, + { + "type": "singleSelect", + "label": "Index", + "validators": [ + { + "type": "string", + "errorMsg": "Length of index name should be between 1 and 80.", + "minLength": 1, + "maxLength": 80 + } + ], + "defaultValue": "default", + "options": { + "endpointUrl": "data/indexes", + "denyList": "^_.*$", + "createSearchChoice": true + }, + "field": "index", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Account", + "options": { + "referenceName": "account" + }, + "help": "", + "field": "account", + "required": true + }, + { + "type": "text", + "label": "Object", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object", + "help": "The name of the object to query for.", + "required": true + }, + { + "type": "text", + "label": "Object Fields", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object_fields", + "help": "Object fields from which to collect data. Delimit multiple fields using a comma.", + "required": true + }, + { + "type": "text", + "label": "Order By", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "LastModifiedDate", + "field": "order_by", + "help": "The datetime field by which to query results in ascending order for indexing.", + "required": true + }, + { + "type": "radio", + "label": "Use existing data input?", + "field": "use_existing_checkpoint", + "defaultValue": "yes", + "help": "Data input already exists. Select `No` if you want to reset the data collection.", + "required": false, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": false + } + }, + { + "type": "text", + "label": "Query Start Date", + "validators": [ + { + "type": "regex", + "errorMsg": "Invalid date and time format", + "pattern": "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}z)?$" + } + ], + "field": "start_date", + "help": "The datetime after which to query and index records, in this format: \"YYYY-MM-DDThh:mm:ss.000z\".\nDefaults to 90 days earlier from now.", + "tooltip": "Changing this parameter may result in gaps or duplication in data collection.", + "required": false + }, + { + "type": "text", + "label": "Limit", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "1000", + "field": "limit", + "help": "The maximum number of results returned by the query.", + "required": false + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Example Input One" + } + ], + "title": "Inputs", + "description": "Manage your data inputs", + "table": { + "actions": [ + "edit", + "enable", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Account Name", + "field": "account" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled" + } + ], + "moreInfo": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled", + "mapping": { + "true": "Disabled", + "false": "Enabled" + } + }, + { + "label": "Example Account", + "field": "account" + }, + { + "label": "Object", + "field": "object" + }, + { + "label": "Object Fields", + "field": "object_fields" + }, + { + "label": "Order By", + "field": "order_by" + }, + { + "label": "Query Start Date", + "field": "start_date" + }, + { + "label": "Limit", + "field": "limit" + } + ] + } + } + }, + "meta": { + "apiVersion": "3.2.0", + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "1.0.0", + "displayName": "Splunk UCC test Add-on" + } +} diff --git a/tests/unit/testdata/invalid_config_both_rest_handler_name_module_are_present.json b/tests/unit/testdata/invalid_config_both_rest_handler_name_module_are_present.json new file mode 100644 index 000000000..d4e7ca50a --- /dev/null +++ b/tests/unit/testdata/invalid_config_both_rest_handler_name_module_are_present.json @@ -0,0 +1,561 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "account", + "table": { + "actions": [ + "edit", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Auth Type", + "field": "auth_type" + } + ] + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "string", + "errorMsg": "Length of ID should be between 1 and 50", + "minLength": 1, + "maxLength": 50 + }, + { + "type": "regex", + "errorMsg": "Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "options": { + "placeholder": "Required" + }, + "field": "name", + "help": "Enter a unique name for this account.", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Environment", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "login.example.com", + "label": "Value1" + }, + { + "value": "test.example.com", + "label": "Value2" + }, + { + "value": "other", + "label": "Other" + } + ], + "display": true + }, + "help": "", + "field": "custom_endpoint", + "defaultValue": "login.example.com", + "required": true + }, + { + "type": "text", + "label": "Endpoint URL", + "help": "Enter the endpoint URL.", + "field": "endpoint", + "options": { + "display": false + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "account_checkbox", + "help": "This is an example checkbox for the account entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "account_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the account entity", + "required": true, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "account_multiple_select", + "help": "This is an example multipleSelect for account entity", + "required": true, + "options": { + "items": [{ + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "text", + "label": "State", + "help": "This is a boolean field for developers to decide whether state parameter will be passed in the OAuth flow. Value: true|false", + "field": "oauth_state_enabled", + "options": { + "display": false + } + }, + { + "type": "oauth", + "field": "oauth", + "label": "Not used", + "options": { + "auth_type": [ + "basic", + "oauth" + ], + "basic": [ + { + "oauth_field": "username", + "label": "Username", + "help": "Enter the username for this account.", + "field": "username" + }, + { + "oauth_field": "password", + "label": "Password", + "encrypted": true, + "help": "Enter the password for this account.", + "field": "password" + }, + { + "oauth_field": "security_token", + "label": "Security Token", + "encrypted": true, + "help": "Enter the security token.", + "field": "token" + } + ], + "oauth": [ + { + "oauth_field": "client_id", + "label": "Client Id", + "field": "client_id", + "help": "Enter the Client Id for this account." + }, + { + "oauth_field": "client_secret", + "label": "Client Secret", + "field": "client_secret", + "encrypted": true, + "help": "Enter the Client Secret key for this account." + }, + { + "oauth_field": "redirect_url", + "label": "Redirect url", + "field": "redirect_url", + "help": "Copy and paste this URL into your app." + } + ], + "auth_code_endpoint": "/services/oauth2/authorize", + "access_token_endpoint": "/services/oauth2/token", + "oauth_timeout": 30 + } + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Account" + } + ], + "title": "Configuration", + "description": "Set up your add-on" + }, + "inputs": { + "services": [ + { + "hook": { + "src": "Hook" + }, + "name": "example_input_one", + "restHandlerName": "custom_rest_handler_file_rh", + "restHandlerModule": "custom_rest_handler_module", + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "input_one_checkbox", + "help": "This is an example checkbox for the input one entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "input_one_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the input one entity", + "required": false, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "field": "singleSelectTest", + "label": "Single Select Group Test", + "type": "singleSelect", + "options": { + "createSearchChoice": true, + "autoCompleteFields": [ + { + "label": "Group1", + "children": [ + { + "value": "one", + "label": "One" + }, + { + "value": "two", + "label": "Two" + } + ] + }, + { + "label": "Group2", + "children": [ + { + "value": "three", + "label": "Three" + }, + { + "value": "four", + "label": "Four" + } + ] + } + ] + } + }, + { + "field": "multipleSelectTest", + "label": "Multiple Select Test", + "type": "multipleSelect", + "options": { + "delimiter": "|", + "items": [ + { + "value": "a", + "label": "A" + }, + { + "value": "b", + "label": "B" + } + ] + } + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds.", + "required": true + }, + { + "type": "singleSelect", + "label": "Index", + "validators": [ + { + "type": "string", + "errorMsg": "Length of index name should be between 1 and 80.", + "minLength": 1, + "maxLength": 80 + } + ], + "defaultValue": "default", + "options": { + "endpointUrl": "data/indexes", + "denyList": "^_.*$", + "createSearchChoice": true + }, + "field": "index", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Account", + "options": { + "referenceName": "account" + }, + "help": "", + "field": "account", + "required": true + }, + { + "type": "text", + "label": "Object", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object", + "help": "The name of the object to query for.", + "required": true + }, + { + "type": "text", + "label": "Object Fields", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object_fields", + "help": "Object fields from which to collect data. Delimit multiple fields using a comma.", + "required": true + }, + { + "type": "text", + "label": "Order By", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "LastModifiedDate", + "field": "order_by", + "help": "The datetime field by which to query results in ascending order for indexing.", + "required": true + }, + { + "type": "radio", + "label": "Use existing data input?", + "field": "use_existing_checkpoint", + "defaultValue": "yes", + "help": "Data input already exists. Select `No` if you want to reset the data collection.", + "required": false, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": false + } + }, + { + "type": "text", + "label": "Query Start Date", + "validators": [ + { + "type": "regex", + "errorMsg": "Invalid date and time format", + "pattern": "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}z)?$" + } + ], + "field": "start_date", + "help": "The datetime after which to query and index records, in this format: \"YYYY-MM-DDThh:mm:ss.000z\".\nDefaults to 90 days earlier from now.", + "tooltip": "Changing this parameter may result in gaps or duplication in data collection.", + "required": false + }, + { + "type": "text", + "label": "Limit", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "1000", + "field": "limit", + "help": "The maximum number of results returned by the query.", + "required": false + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Example Input One" + } + ], + "title": "Inputs", + "description": "Manage your data inputs", + "table": { + "actions": [ + "edit", + "enable", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Account Name", + "field": "account" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled" + } + ], + "moreInfo": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled", + "mapping": { + "true": "Disabled", + "false": "Enabled" + } + }, + { + "label": "Example Account", + "field": "account" + }, + { + "label": "Object", + "field": "object" + }, + { + "label": "Object Fields", + "field": "object_fields" + }, + { + "label": "Order By", + "field": "order_by" + }, + { + "label": "Query Start Date", + "field": "start_date" + }, + { + "label": "Limit", + "field": "limit" + } + ] + } + } + }, + "meta": { + "apiVersion": "3.2.0", + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "1.0.0", + "displayName": "Splunk UCC test Add-on" + } +} diff --git a/tests/unit/testdata/invalid_config_json_is_missing_in_supported_file_types.json b/tests/unit/testdata/invalid_config_json_is_missing_in_supported_file_types.json new file mode 100644 index 000000000..7c41f5969 --- /dev/null +++ b/tests/unit/testdata/invalid_config_json_is_missing_in_supported_file_types.json @@ -0,0 +1,236 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "account", + "table": { + "actions": [ + "edit", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Auth Type", + "field": "auth_type" + } + ] + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "string", + "errorMsg": "Length of ID should be between 1 and 50", + "minLength": 1, + "maxLength": 50 + }, + { + "type": "regex", + "errorMsg": "Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "options": { + "placeholder": "Required" + }, + "field": "name", + "help": "Enter a unique name for this account.", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Environment", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "login.example.com", + "label": "Value1" + }, + { + "value": "test.example.com", + "label": "Value2" + }, + { + "value": "other", + "label": "Other" + } + ], + "display": true + }, + "help": "", + "field": "custom_endpoint", + "defaultValue": "login.example.com", + "required": true + }, + { + "type": "text", + "label": "Endpoint URL", + "help": "Enter the endpoint URL.", + "field": "endpoint", + "options": { + "display": false + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "account_checkbox", + "help": "This is an example checkbox for the account entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "account_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the account entity", + "required": true, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "account_multiple_select", + "help": "This is an example multipleSelect for account entity", + "required": true, + "options": { + "items": [{ + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "text", + "label": "State", + "help": "This is a boolean field for developers to decide whether state parameter will be passed in the OAuth flow. Value: true|false", + "field": "oauth_state_enabled", + "options": { + "display": false + } + }, + { + "type": "oauth", + "field": "oauth", + "label": "Not used", + "options": { + "auth_type": [ + "basic", + "oauth" + ], + "basic": [ + { + "oauth_field": "username", + "label": "Username", + "help": "Enter the username for this account.", + "field": "username" + }, + { + "oauth_field": "password", + "label": "Password", + "encrypted": true, + "help": "Enter the password for this account.", + "field": "password" + }, + { + "oauth_field": "security_token", + "label": "Security Token", + "encrypted": true, + "help": "Enter the security token.", + "field": "token" + } + ], + "oauth": [ + { + "oauth_field": "client_id", + "label": "Client Id", + "field": "client_id", + "help": "Enter the Client Id for this account." + }, + { + "oauth_field": "client_secret", + "label": "Client Secret", + "field": "client_secret", + "encrypted": true, + "help": "Enter the Client Secret key for this account." + }, + { + "oauth_field": "redirect_url", + "label": "Redirect url", + "field": "redirect_url", + "help": "Copy and paste this URL into your app." + } + ], + "auth_code_endpoint": "/services/oauth2/authorize", + "access_token_endpoint": "/services/oauth2/token", + "oauth_timeout": 30 + } + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + }, + { + "type": "file", + "label": "Upload File", + "help": "Upload service account's certificate", + "field": "service_account", + "options": { + "fileSupportMessage": "Here is the support message" + }, + "validators": [ + { + "type": "file", + "supportedFileTypes": [ + "yaml" + ] + } + ], + "encrypted": true, + "required": true + } + ], + "title": "Account" + } + ], + "title": "Configuration", + "description": "Set up your add-on" + } + }, + "meta": { + "apiVersion": "3.2.0", + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "1.0.0", + "displayName": "Splunk UCC test Add-on" + } +} diff --git a/tests/unit/testdata/invalid_config_only_rest_handler_class_is_present.json b/tests/unit/testdata/invalid_config_only_rest_handler_class_is_present.json new file mode 100644 index 000000000..b148fb770 --- /dev/null +++ b/tests/unit/testdata/invalid_config_only_rest_handler_class_is_present.json @@ -0,0 +1,560 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "account", + "table": { + "actions": [ + "edit", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Auth Type", + "field": "auth_type" + } + ] + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "string", + "errorMsg": "Length of ID should be between 1 and 50", + "minLength": 1, + "maxLength": 50 + }, + { + "type": "regex", + "errorMsg": "Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "options": { + "placeholder": "Required" + }, + "field": "name", + "help": "Enter a unique name for this account.", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Environment", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "login.example.com", + "label": "Value1" + }, + { + "value": "test.example.com", + "label": "Value2" + }, + { + "value": "other", + "label": "Other" + } + ], + "display": true + }, + "help": "", + "field": "custom_endpoint", + "defaultValue": "login.example.com", + "required": true + }, + { + "type": "text", + "label": "Endpoint URL", + "help": "Enter the endpoint URL.", + "field": "endpoint", + "options": { + "display": false + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "account_checkbox", + "help": "This is an example checkbox for the account entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "account_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the account entity", + "required": true, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "account_multiple_select", + "help": "This is an example multipleSelect for account entity", + "required": true, + "options": { + "items": [{ + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "text", + "label": "State", + "help": "This is a boolean field for developers to decide whether state parameter will be passed in the OAuth flow. Value: true|false", + "field": "oauth_state_enabled", + "options": { + "display": false + } + }, + { + "type": "oauth", + "field": "oauth", + "label": "Not used", + "options": { + "auth_type": [ + "basic", + "oauth" + ], + "basic": [ + { + "oauth_field": "username", + "label": "Username", + "help": "Enter the username for this account.", + "field": "username" + }, + { + "oauth_field": "password", + "label": "Password", + "encrypted": true, + "help": "Enter the password for this account.", + "field": "password" + }, + { + "oauth_field": "security_token", + "label": "Security Token", + "encrypted": true, + "help": "Enter the security token.", + "field": "token" + } + ], + "oauth": [ + { + "oauth_field": "client_id", + "label": "Client Id", + "field": "client_id", + "help": "Enter the Client Id for this account." + }, + { + "oauth_field": "client_secret", + "label": "Client Secret", + "field": "client_secret", + "encrypted": true, + "help": "Enter the Client Secret key for this account." + }, + { + "oauth_field": "redirect_url", + "label": "Redirect url", + "field": "redirect_url", + "help": "Copy and paste this URL into your app." + } + ], + "auth_code_endpoint": "/services/oauth2/authorize", + "access_token_endpoint": "/services/oauth2/token", + "oauth_timeout": 30 + } + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Account" + } + ], + "title": "Configuration", + "description": "Set up your add-on" + }, + "inputs": { + "services": [ + { + "hook": { + "src": "Hook" + }, + "name": "example_input_one", + "restHandlerClass": "CustomRestHandlerClass", + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "input_one_checkbox", + "help": "This is an example checkbox for the input one entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "input_one_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the input one entity", + "required": false, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "field": "singleSelectTest", + "label": "Single Select Group Test", + "type": "singleSelect", + "options": { + "createSearchChoice": true, + "autoCompleteFields": [ + { + "label": "Group1", + "children": [ + { + "value": "one", + "label": "One" + }, + { + "value": "two", + "label": "Two" + } + ] + }, + { + "label": "Group2", + "children": [ + { + "value": "three", + "label": "Three" + }, + { + "value": "four", + "label": "Four" + } + ] + } + ] + } + }, + { + "field": "multipleSelectTest", + "label": "Multiple Select Test", + "type": "multipleSelect", + "options": { + "delimiter": "|", + "items": [ + { + "value": "a", + "label": "A" + }, + { + "value": "b", + "label": "B" + } + ] + } + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds.", + "required": true + }, + { + "type": "singleSelect", + "label": "Index", + "validators": [ + { + "type": "string", + "errorMsg": "Length of index name should be between 1 and 80.", + "minLength": 1, + "maxLength": 80 + } + ], + "defaultValue": "default", + "options": { + "endpointUrl": "data/indexes", + "denyList": "^_.*$", + "createSearchChoice": true + }, + "field": "index", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Account", + "options": { + "referenceName": "account" + }, + "help": "", + "field": "account", + "required": true + }, + { + "type": "text", + "label": "Object", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object", + "help": "The name of the object to query for.", + "required": true + }, + { + "type": "text", + "label": "Object Fields", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object_fields", + "help": "Object fields from which to collect data. Delimit multiple fields using a comma.", + "required": true + }, + { + "type": "text", + "label": "Order By", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "LastModifiedDate", + "field": "order_by", + "help": "The datetime field by which to query results in ascending order for indexing.", + "required": true + }, + { + "type": "radio", + "label": "Use existing data input?", + "field": "use_existing_checkpoint", + "defaultValue": "yes", + "help": "Data input already exists. Select `No` if you want to reset the data collection.", + "required": false, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": false + } + }, + { + "type": "text", + "label": "Query Start Date", + "validators": [ + { + "type": "regex", + "errorMsg": "Invalid date and time format", + "pattern": "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}z)?$" + } + ], + "field": "start_date", + "help": "The datetime after which to query and index records, in this format: \"YYYY-MM-DDThh:mm:ss.000z\".\nDefaults to 90 days earlier from now.", + "tooltip": "Changing this parameter may result in gaps or duplication in data collection.", + "required": false + }, + { + "type": "text", + "label": "Limit", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "1000", + "field": "limit", + "help": "The maximum number of results returned by the query.", + "required": false + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Example Input One" + } + ], + "title": "Inputs", + "description": "Manage your data inputs", + "table": { + "actions": [ + "edit", + "enable", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Account Name", + "field": "account" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled" + } + ], + "moreInfo": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled", + "mapping": { + "true": "Disabled", + "false": "Enabled" + } + }, + { + "label": "Example Account", + "field": "account" + }, + { + "label": "Object", + "field": "object" + }, + { + "label": "Object Fields", + "field": "object_fields" + }, + { + "label": "Order By", + "field": "order_by" + }, + { + "label": "Query Start Date", + "field": "start_date" + }, + { + "label": "Limit", + "field": "limit" + } + ] + } + } + }, + "meta": { + "apiVersion": "3.2.0", + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "1.0.0", + "displayName": "Splunk UCC test Add-on" + } +} diff --git a/tests/unit/testdata/invalid_config_only_rest_handler_module_is_present.json b/tests/unit/testdata/invalid_config_only_rest_handler_module_is_present.json new file mode 100644 index 000000000..17acae161 --- /dev/null +++ b/tests/unit/testdata/invalid_config_only_rest_handler_module_is_present.json @@ -0,0 +1,560 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "account", + "table": { + "actions": [ + "edit", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Auth Type", + "field": "auth_type" + } + ] + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "string", + "errorMsg": "Length of ID should be between 1 and 50", + "minLength": 1, + "maxLength": 50 + }, + { + "type": "regex", + "errorMsg": "Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "options": { + "placeholder": "Required" + }, + "field": "name", + "help": "Enter a unique name for this account.", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Environment", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "login.example.com", + "label": "Value1" + }, + { + "value": "test.example.com", + "label": "Value2" + }, + { + "value": "other", + "label": "Other" + } + ], + "display": true + }, + "help": "", + "field": "custom_endpoint", + "defaultValue": "login.example.com", + "required": true + }, + { + "type": "text", + "label": "Endpoint URL", + "help": "Enter the endpoint URL.", + "field": "endpoint", + "options": { + "display": false + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "account_checkbox", + "help": "This is an example checkbox for the account entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "account_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the account entity", + "required": true, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "account_multiple_select", + "help": "This is an example multipleSelect for account entity", + "required": true, + "options": { + "items": [{ + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "text", + "label": "State", + "help": "This is a boolean field for developers to decide whether state parameter will be passed in the OAuth flow. Value: true|false", + "field": "oauth_state_enabled", + "options": { + "display": false + } + }, + { + "type": "oauth", + "field": "oauth", + "label": "Not used", + "options": { + "auth_type": [ + "basic", + "oauth" + ], + "basic": [ + { + "oauth_field": "username", + "label": "Username", + "help": "Enter the username for this account.", + "field": "username" + }, + { + "oauth_field": "password", + "label": "Password", + "encrypted": true, + "help": "Enter the password for this account.", + "field": "password" + }, + { + "oauth_field": "security_token", + "label": "Security Token", + "encrypted": true, + "help": "Enter the security token.", + "field": "token" + } + ], + "oauth": [ + { + "oauth_field": "client_id", + "label": "Client Id", + "field": "client_id", + "help": "Enter the Client Id for this account." + }, + { + "oauth_field": "client_secret", + "label": "Client Secret", + "field": "client_secret", + "encrypted": true, + "help": "Enter the Client Secret key for this account." + }, + { + "oauth_field": "redirect_url", + "label": "Redirect url", + "field": "redirect_url", + "help": "Copy and paste this URL into your app." + } + ], + "auth_code_endpoint": "/services/oauth2/authorize", + "access_token_endpoint": "/services/oauth2/token", + "oauth_timeout": 30 + } + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Account" + } + ], + "title": "Configuration", + "description": "Set up your add-on" + }, + "inputs": { + "services": [ + { + "hook": { + "src": "Hook" + }, + "name": "example_input_one", + "restHandlerModule": "custom_rest_handler_module", + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "input_one_checkbox", + "help": "This is an example checkbox for the input one entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "input_one_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the input one entity", + "required": false, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "field": "singleSelectTest", + "label": "Single Select Group Test", + "type": "singleSelect", + "options": { + "createSearchChoice": true, + "autoCompleteFields": [ + { + "label": "Group1", + "children": [ + { + "value": "one", + "label": "One" + }, + { + "value": "two", + "label": "Two" + } + ] + }, + { + "label": "Group2", + "children": [ + { + "value": "three", + "label": "Three" + }, + { + "value": "four", + "label": "Four" + } + ] + } + ] + } + }, + { + "field": "multipleSelectTest", + "label": "Multiple Select Test", + "type": "multipleSelect", + "options": { + "delimiter": "|", + "items": [ + { + "value": "a", + "label": "A" + }, + { + "value": "b", + "label": "B" + } + ] + } + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds.", + "required": true + }, + { + "type": "singleSelect", + "label": "Index", + "validators": [ + { + "type": "string", + "errorMsg": "Length of index name should be between 1 and 80.", + "minLength": 1, + "maxLength": 80 + } + ], + "defaultValue": "default", + "options": { + "endpointUrl": "data/indexes", + "denyList": "^_.*$", + "createSearchChoice": true + }, + "field": "index", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Account", + "options": { + "referenceName": "account" + }, + "help": "", + "field": "account", + "required": true + }, + { + "type": "text", + "label": "Object", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object", + "help": "The name of the object to query for.", + "required": true + }, + { + "type": "text", + "label": "Object Fields", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object_fields", + "help": "Object fields from which to collect data. Delimit multiple fields using a comma.", + "required": true + }, + { + "type": "text", + "label": "Order By", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "LastModifiedDate", + "field": "order_by", + "help": "The datetime field by which to query results in ascending order for indexing.", + "required": true + }, + { + "type": "radio", + "label": "Use existing data input?", + "field": "use_existing_checkpoint", + "defaultValue": "yes", + "help": "Data input already exists. Select `No` if you want to reset the data collection.", + "required": false, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": false + } + }, + { + "type": "text", + "label": "Query Start Date", + "validators": [ + { + "type": "regex", + "errorMsg": "Invalid date and time format", + "pattern": "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}z)?$" + } + ], + "field": "start_date", + "help": "The datetime after which to query and index records, in this format: \"YYYY-MM-DDThh:mm:ss.000z\".\nDefaults to 90 days earlier from now.", + "tooltip": "Changing this parameter may result in gaps or duplication in data collection.", + "required": false + }, + { + "type": "text", + "label": "Limit", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "1000", + "field": "limit", + "help": "The maximum number of results returned by the query.", + "required": false + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Example Input One" + } + ], + "title": "Inputs", + "description": "Manage your data inputs", + "table": { + "actions": [ + "edit", + "enable", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Account Name", + "field": "account" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled" + } + ], + "moreInfo": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled", + "mapping": { + "true": "Disabled", + "false": "Enabled" + } + }, + { + "label": "Example Account", + "field": "account" + }, + { + "label": "Object", + "field": "object" + }, + { + "label": "Object Fields", + "field": "object_fields" + }, + { + "label": "Order By", + "field": "order_by" + }, + { + "label": "Query Start Date", + "field": "start_date" + }, + { + "label": "Limit", + "field": "limit" + } + ] + } + } + }, + "meta": { + "apiVersion": "3.2.0", + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "1.0.0", + "displayName": "Splunk UCC test Add-on" + } +} diff --git a/tests/unit/testdata/invalid_config_supported_file_types_field_is_missing.json b/tests/unit/testdata/invalid_config_supported_file_types_field_is_missing.json new file mode 100644 index 000000000..4eb5b94cf --- /dev/null +++ b/tests/unit/testdata/invalid_config_supported_file_types_field_is_missing.json @@ -0,0 +1,233 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "account", + "table": { + "actions": [ + "edit", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Auth Type", + "field": "auth_type" + } + ] + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "string", + "errorMsg": "Length of ID should be between 1 and 50", + "minLength": 1, + "maxLength": 50 + }, + { + "type": "regex", + "errorMsg": "Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "options": { + "placeholder": "Required" + }, + "field": "name", + "help": "Enter a unique name for this account.", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Environment", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "login.example.com", + "label": "Value1" + }, + { + "value": "test.example.com", + "label": "Value2" + }, + { + "value": "other", + "label": "Other" + } + ], + "display": true + }, + "help": "", + "field": "custom_endpoint", + "defaultValue": "login.example.com", + "required": true + }, + { + "type": "text", + "label": "Endpoint URL", + "help": "Enter the endpoint URL.", + "field": "endpoint", + "options": { + "display": false + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "account_checkbox", + "help": "This is an example checkbox for the account entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "account_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the account entity", + "required": true, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "account_multiple_select", + "help": "This is an example multipleSelect for account entity", + "required": true, + "options": { + "items": [{ + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "text", + "label": "State", + "help": "This is a boolean field for developers to decide whether state parameter will be passed in the OAuth flow. Value: true|false", + "field": "oauth_state_enabled", + "options": { + "display": false + } + }, + { + "type": "oauth", + "field": "oauth", + "label": "Not used", + "options": { + "auth_type": [ + "basic", + "oauth" + ], + "basic": [ + { + "oauth_field": "username", + "label": "Username", + "help": "Enter the username for this account.", + "field": "username" + }, + { + "oauth_field": "password", + "label": "Password", + "encrypted": true, + "help": "Enter the password for this account.", + "field": "password" + }, + { + "oauth_field": "security_token", + "label": "Security Token", + "encrypted": true, + "help": "Enter the security token.", + "field": "token" + } + ], + "oauth": [ + { + "oauth_field": "client_id", + "label": "Client Id", + "field": "client_id", + "help": "Enter the Client Id for this account." + }, + { + "oauth_field": "client_secret", + "label": "Client Secret", + "field": "client_secret", + "encrypted": true, + "help": "Enter the Client Secret key for this account." + }, + { + "oauth_field": "redirect_url", + "label": "Redirect url", + "field": "redirect_url", + "help": "Copy and paste this URL into your app." + } + ], + "auth_code_endpoint": "/services/oauth2/authorize", + "access_token_endpoint": "/services/oauth2/token", + "oauth_timeout": 30 + } + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + }, + { + "type": "file", + "label": "Upload File", + "help": "Upload service account's certificate", + "field": "service_account", + "options": { + "fileSupportMessage": "Here is the support message" + }, + "validators": [ + { + "type": "file" + } + ], + "encrypted": true, + "required": true + } + ], + "title": "Account" + } + ], + "title": "Configuration", + "description": "Set up your add-on" + } + }, + "meta": { + "apiVersion": "3.2.0", + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "1.0.0", + "displayName": "Splunk UCC test Add-on" + } +} diff --git a/tests/unit/testdata/invalid_config_validators_missing_for_file_input.json b/tests/unit/testdata/invalid_config_validators_missing_for_file_input.json new file mode 100644 index 000000000..728872cad --- /dev/null +++ b/tests/unit/testdata/invalid_config_validators_missing_for_file_input.json @@ -0,0 +1,228 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "account", + "table": { + "actions": [ + "edit", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Auth Type", + "field": "auth_type" + } + ] + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "string", + "errorMsg": "Length of ID should be between 1 and 50", + "minLength": 1, + "maxLength": 50 + }, + { + "type": "regex", + "errorMsg": "Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "options": { + "placeholder": "Required" + }, + "field": "name", + "help": "Enter a unique name for this account.", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Environment", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "login.example.com", + "label": "Value1" + }, + { + "value": "test.example.com", + "label": "Value2" + }, + { + "value": "other", + "label": "Other" + } + ], + "display": true + }, + "help": "", + "field": "custom_endpoint", + "defaultValue": "login.example.com", + "required": true + }, + { + "type": "text", + "label": "Endpoint URL", + "help": "Enter the endpoint URL.", + "field": "endpoint", + "options": { + "display": false + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "account_checkbox", + "help": "This is an example checkbox for the account entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "account_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the account entity", + "required": true, + "options": { + "items": [{ + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "account_multiple_select", + "help": "This is an example multipleSelect for account entity", + "required": true, + "options": { + "items": [{ + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "text", + "label": "State", + "help": "This is a boolean field for developers to decide whether state parameter will be passed in the OAuth flow. Value: true|false", + "field": "oauth_state_enabled", + "options": { + "display": false + } + }, + { + "type": "oauth", + "field": "oauth", + "label": "Not used", + "options": { + "auth_type": [ + "basic", + "oauth" + ], + "basic": [ + { + "oauth_field": "username", + "label": "Username", + "help": "Enter the username for this account.", + "field": "username" + }, + { + "oauth_field": "password", + "label": "Password", + "encrypted": true, + "help": "Enter the password for this account.", + "field": "password" + }, + { + "oauth_field": "security_token", + "label": "Security Token", + "encrypted": true, + "help": "Enter the security token.", + "field": "token" + } + ], + "oauth": [ + { + "oauth_field": "client_id", + "label": "Client Id", + "field": "client_id", + "help": "Enter the Client Id for this account." + }, + { + "oauth_field": "client_secret", + "label": "Client Secret", + "field": "client_secret", + "encrypted": true, + "help": "Enter the Client Secret key for this account." + }, + { + "oauth_field": "redirect_url", + "label": "Redirect url", + "field": "redirect_url", + "help": "Copy and paste this URL into your app." + } + ], + "auth_code_endpoint": "/services/oauth2/authorize", + "access_token_endpoint": "/services/oauth2/token", + "oauth_timeout": 30 + } + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + }, + { + "type": "file", + "label": "Upload File", + "help": "Upload service account's certificate", + "field": "service_account", + "options": { + "fileSupportMessage": "Here is the support message" + }, + "encrypted": true, + "required": true + } + ], + "title": "Account" + } + ], + "title": "Configuration", + "description": "Set up your add-on" + } + }, + "meta": { + "apiVersion": "3.2.0", + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "1.0.0", + "displayName": "Splunk UCC test Add-on" + } +} diff --git a/tests/unit/testdata/valid_config.json b/tests/unit/testdata/valid_config.json index 0ffcfbc27..cc5a93777 100644 --- a/tests/unit/testdata/valid_config.json +++ b/tests/unit/testdata/valid_config.json @@ -198,6 +198,34 @@ "text": "Help Link", "link": "https://docs.splunk.com/Documentation" } + }, + { + "type": "textarea", + "label": "Textarea Field", + "field": "textarea_field", + "help": "Help message", + "options": { + "rowsMin": 3, + "rowsMax": 15 + }, + "required": true + }, + { + "type": "file", + "label": "Upload File", + "help": "Upload service account's certificate", + "field": "service_account", + "options": { + "fileSupportMessage": "Here is the support message" + }, + "validators": [ + { + "type": "file", + "supportedFileTypes": ["json"] + } + ], + "encrypted": true, + "required": true } ], "title": "Account" diff --git a/tests/unit/testdata/valid_config.yaml b/tests/unit/testdata/valid_config.yaml index 693381382..74d0fcd6a 100644 --- a/tests/unit/testdata/valid_config.yaml +++ b/tests/unit/testdata/valid_config.yaml @@ -132,6 +132,25 @@ pages: options: text: Help Link link: https://docs.splunk.com/Documentation + - type: textarea + field: textarea_field + label: Textarea Field + help: Help message + options: + rowsMin: 3 + rowsMax: 15 + - type: file + field: service_account + help: Upload service account's certificate + encrypted: true + required: true + label: Upload file + options: + fileSupportMessage: Here is the support message + validators: + - type: file + supportedFileTypes: + - json title: Account - name: proxy entity: diff --git a/tests/unit/testdata/validator_builder_result_date b/tests/unit/testdata/validator_builder_result_date new file mode 100644 index 000000000..c91db285a --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_date @@ -0,0 +1,3 @@ +validator.Pattern( + regex=r"""^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$""", +) \ No newline at end of file diff --git a/tests/unit/testdata/validator_builder_result_email b/tests/unit/testdata/validator_builder_result_email new file mode 100644 index 000000000..2fd0ce437 --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_email @@ -0,0 +1,3 @@ +validator.Pattern( + regex=r"""^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""", +) \ No newline at end of file diff --git a/tests/unit/testdata/validator_builder_result_everything b/tests/unit/testdata/validator_builder_result_everything new file mode 100644 index 000000000..a48f04772 --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_everything @@ -0,0 +1,25 @@ +validator.AllOf( + validator.Pattern( + regex=r"""^[a-zA-Z]\w*$""", + ), + validator.String( + max_len=100, + min_len=1, + ), + validator.Number( + max_val=65535, + min_val=1, + ), + validator.Pattern( + regex=r"""^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""", + ), + validator.Pattern( + regex=r"""^(?:(?:[0-1]?\d{1,2}|2[0-4]\d|25[0-5])(?:\.|$)){4}$""", + ), + validator.Pattern( + regex=r"""^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$""", + ), + validator.Pattern( + regex=r"""^(?:(?:https?|ftp|opc\.tcp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?_?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))?)(?::\d{2,5})?(?:\/[^\s]*)?$""", + ) +) \ No newline at end of file diff --git a/tests/unit/testdata/validator_builder_result_ipv4 b/tests/unit/testdata/validator_builder_result_ipv4 new file mode 100644 index 000000000..69556dd72 --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_ipv4 @@ -0,0 +1,3 @@ +validator.Pattern( + regex=r"""^(?:(?:[0-1]?\d{1,2}|2[0-4]\d|25[0-5])(?:\.|$)){4}$""", +) \ No newline at end of file diff --git a/tests/unit/testdata/validator_builder_result_number b/tests/unit/testdata/validator_builder_result_number new file mode 100644 index 000000000..89d856b95 --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_number @@ -0,0 +1,4 @@ +validator.Number( + max_val=65535, + min_val=1, +) \ No newline at end of file diff --git a/tests/unit/testdata/validator_builder_result_regex b/tests/unit/testdata/validator_builder_result_regex new file mode 100644 index 000000000..eed7835fd --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_regex @@ -0,0 +1,3 @@ +validator.Pattern( + regex=r"""^[a-zA-Z]\w*$""", +) \ No newline at end of file diff --git a/tests/unit/testdata/validator_builder_result_string b/tests/unit/testdata/validator_builder_result_string new file mode 100644 index 000000000..9032e2f81 --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_string @@ -0,0 +1,4 @@ +validator.String( + max_len=100, + min_len=1, +) \ No newline at end of file diff --git a/tests/unit/testdata/validator_builder_result_string_regex b/tests/unit/testdata/validator_builder_result_string_regex new file mode 100644 index 000000000..2ad9be4ca --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_string_regex @@ -0,0 +1,9 @@ +validator.AllOf( + validator.Pattern( + regex=r"""^[a-zA-Z]\w*$""", + ), + validator.String( + max_len=100, + min_len=1, + ) +) \ No newline at end of file diff --git a/tests/unit/testdata/validator_builder_result_url b/tests/unit/testdata/validator_builder_result_url new file mode 100644 index 000000000..5922dbec3 --- /dev/null +++ b/tests/unit/testdata/validator_builder_result_url @@ -0,0 +1,3 @@ +validator.Pattern( + regex=r"""^(?:(?:https?|ftp|opc\.tcp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?_?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))?)(?::\d{2,5})?(?:\/[^\s]*)?$""", +) \ No newline at end of file