insightconnect-plugin-runtime 6.3.0__py3-none-any.whl → 6.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,9 +24,11 @@ from insightconnect_plugin_runtime.exceptions import (
24
24
  ServerException,
25
25
  )
26
26
  from insightconnect_plugin_runtime.util import OutputMasker
27
+ from uuid import UUID
27
28
 
28
29
  logger = structlog.get_logger("plugin")
29
30
  ORG_ID = "X-IPIMS-ORGID"
31
+ INT_ID = "X-INTEGRATION-ID"
30
32
 
31
33
  PLUGIN_SPEC_DOCKERFILE = "/python/src/plugin.spec.yaml"
32
34
  PLUGIN_SPEC_PACK = "/workspace/plugin.spec.yaml"
@@ -173,15 +175,20 @@ class Endpoints:
173
175
  500:
174
176
  description: Unexpected error
175
177
  """
178
+
176
179
  self.logger.info("Plugin task beginning execution...")
177
180
  input_message = request.get_json(force=True)
178
181
  self.logger.debug("Request input: %s", input_message)
179
182
  Endpoints.validate_action_trigger_task_empty_input(input_message)
180
183
  Endpoints.validate_action_trigger_task_name(input_message, name, "task")
184
+
181
185
  # No validation on the plugin custom config to leave this as configurable as possible.
182
186
  # `add_plugin_custom_config` will pass any available values to the plugin for interpretation.
183
187
  input_message = self.add_plugin_custom_config(
184
- input_message, request.headers.get(ORG_ID)
188
+ input_message,
189
+ request.headers.get(ORG_ID, ""),
190
+ request.headers.get(INT_ID, ""),
191
+ name,
185
192
  )
186
193
  output = self.run_action_trigger_task(input_message, mask_output=False)
187
194
  self.logger.info("Plugin task finished execution...")
@@ -787,7 +794,7 @@ class Endpoints:
787
794
  return version
788
795
 
789
796
  def add_plugin_custom_config(
790
- self, input_data: Dict[str, Any], org_id: str
797
+ self, input_data: Dict[str, Any], org_id: str, int_id: str, task_name: str
791
798
  ) -> Dict[str, Any]:
792
799
  """
793
800
  Using the retrieved configs pulled from komand-props, pass the configuration that matches the requesting
@@ -800,22 +807,100 @@ class Endpoints:
800
807
  - org 1 when in lookback mode will pull back 108 hours (task triggered with no state).
801
808
  - all orgs for default runs will poll back 12 hours.
802
809
  - all orgs in lookback mode will be 100 hours (task triggered with no state).
810
+
811
+ When Int ID is specified or task name. Then it can be used across all orgs (global config) or specific Org ID.
812
+ There's a hierarchy between task related config and Int ID related, where Int ID will always
813
+ replace task related config due to its higher priority.
814
+
815
+ Config example:
816
+ {
817
+ "org_1": {
818
+ "task_name_1": {
819
+ "default": 12,
820
+ "lookback": "100"
821
+ },
822
+ "int_1": {
823
+ "default": 6,
824
+ "lookback": "50"
825
+ },
826
+ {
827
+ "default": 24,
828
+ "lookback": "108"
829
+ }
830
+ },
831
+ "*": {
832
+ "default": 24,
833
+ "lookback": 200
834
+ "task_name_2": {
835
+ "default": 2,
836
+ "lookback": "10"
837
+ }
838
+ }
839
+ }
840
+
841
+ In this config example the following we be applied:
842
+ - org 1 to has a custom default time of 24 hours for their timings.
843
+ - org 1 when in lookback mode will pull back 108 hours (task triggered with no state).
844
+ - org 1 all integrations that uses task with 'task_name_1' will pull back 100 hours
845
+ and have a custom default time of 12 hours for their timings.
846
+ - org 1 specific integration 'int_1' will pull back 50 hours and have a custom
847
+ default time of 6 hours for their timings.
848
+ - all orgs for default runs will poll back 24 hours.
849
+ - all orgs in lookback mode will be 200 hours (task triggered with no state).
850
+ - all orgs that uses task with 'task_name_2' will pull back 2 hours
851
+ - all orgs that uses task with 'task_name_2' in lookback mode will be 10 hours
803
852
  """
804
- additional_config = self.config_options.get(org_id) or self.config_options.get(
805
- "*"
806
- )
853
+
854
+ # Parse configuration based on organization or global. Use its copies, not to modify original dict.
855
+ organization_config, global_config = self.config_options.get(org_id, {}).copy(), self.config_options.get("*", {}).copy()
856
+
857
+ # Definition of additional config and its type variables.
858
+ additional_config, config_type = {}, ""
859
+
860
+ # Check if we have a global config.
861
+ # Also, use "config_type" variable just for logging purposes to have an indicator from where the config was pulled.
862
+ if global_config:
863
+ # Setup global config as starting point.
864
+ additional_config, config_type = global_config, "GLOBAL"
865
+
866
+ # If task configuration was found under global config, update it with task config to replace necessary values.
867
+ if task_config := global_config.get(task_name):
868
+ additional_config.update(task_config)
869
+ config_type = "GLOBAL_TASK"
870
+
871
+ # If organization config is present, then replace its values with the ones coming from global config.
872
+ if organization_config:
873
+ # Update additional config with values coming from organization config.
874
+ additional_config.update(organization_config)
875
+ config_type = "ORG"
876
+
877
+ # If task configuration was found under organization, replace its values with organization config.
878
+ if task_config := organization_config.get(task_name):
879
+ additional_config.update(task_config)
880
+ config_type = "ORG_TASK"
881
+
882
+ # If integration config was found under organization replace its values with organization
883
+ # and task config (higher priority).
884
+ if integration_config := organization_config.get(int_id):
885
+ additional_config.update(integration_config)
886
+ config_type = "ORG_INT"
887
+
807
888
  if additional_config:
808
889
  self.logger.info(
809
- "Found config options; adding this to the request parameters..."
890
+ f"Found config options ({config_type}); adding this to the request parameters..."
810
891
  )
811
- additional_config = (
812
- additional_config.copy()
813
- ) # copy to preserve the referenced value in self.config_options
892
+
893
+ # Sopy to preserve the referenced value in self.config_options.
894
+ # Also, remove unnecessary fields from that config (int_ids, or task_names) that occurs
895
+ # when updated global dictionary above. This thing is that we use global config as base
896
+ # and then updating some fields depending on the configuration.
897
+ additional_config = self._remove_unnecessary_fields_from_custom_config(additional_config)
898
+
814
899
  # As a safeguard we only pass the lookback config params if the plugin has no state
815
900
  # This means we still need to manually delete the state for plugins on a per org basis.
816
901
  # This also means first time customers for their 'initial' lookup would get the lookback value passed in.
817
902
  if input_data.get("body", {}).get("state") and additional_config.get(
818
- "lookback"
903
+ "lookback"
819
904
  ):
820
905
  self.logger.info(
821
906
  "Found an existing plugin state, not passing lookback value..."
@@ -842,8 +927,8 @@ class Endpoints:
842
927
  if isinstance(wrapped_exception, ClientException):
843
928
  status_code = 400
844
929
  elif (
845
- isinstance(wrapped_exception, PluginException)
846
- and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
930
+ isinstance(wrapped_exception, PluginException)
931
+ and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
847
932
  ):
848
933
  status_code = 400
849
934
  elif isinstance(wrapped_exception, (ConnectionTestException, ClientException)):
@@ -868,8 +953,8 @@ class Endpoints:
868
953
  if isinstance(wrapped_exception, (ConnectionTestException, ClientException)):
869
954
  return 400
870
955
  elif (
871
- isinstance(wrapped_exception, PluginException)
872
- and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
956
+ isinstance(wrapped_exception, PluginException)
957
+ and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
873
958
  ):
874
959
  return 400
875
960
  elif isinstance(wrapped_exception, ServerException):
@@ -878,3 +963,46 @@ class Endpoints:
878
963
  return 501
879
964
  else:
880
965
  return 500
966
+
967
+ def _remove_unnecessary_fields_from_custom_config(self, additional_config: Dict[str, Any]) -> Dict[str, Any]:
968
+ """
969
+ Removes unnecessary fields from custom config such as other task names, and int_ids,
970
+ leaving only the fields that needs to be parsed.
971
+
972
+ :param additional_config: The custom config dictionary on which, unnecessary fields will be removed.
973
+ :type additional_config: Dict[str, Any]
974
+
975
+ :return: New custom config dictionary with unnecessary fields removed.
976
+ :rtype: Dict[str, Any]
977
+ """
978
+
979
+ # Copy 'additional_config' not to operate on it
980
+ config_copy = additional_config.copy()
981
+
982
+ # Remove other task names from config
983
+ for task_ in self.plugin.tasks.keys():
984
+ config_copy.pop(task_, None)
985
+
986
+ # Remove other int_ids from config
987
+ for key_ in additional_config.keys():
988
+ if self._check_if_uuid(key_):
989
+ config_copy.pop(key_, None)
990
+ return config_copy
991
+
992
+ @staticmethod
993
+ def _check_if_uuid(input_string: str) -> bool:
994
+ """
995
+ Validates whether the provided string matches UUID format specifications.
996
+
997
+ :param input_string: The string to validate against UUID format standards
998
+ :type input_string: str
999
+
1000
+ :return: True if the string is a valid UUID, False otherwise
1001
+ :rtype: bool
1002
+ """
1003
+
1004
+ try:
1005
+ UUID(input_string)
1006
+ return True
1007
+ except (TypeError, ValueError):
1008
+ return False
@@ -262,7 +262,10 @@ def response_handler(
262
262
  ) -> None:
263
263
  """
264
264
  Check response status codes and return a generic PluginException preset if a HTTPError is raised.
265
- Excetion cause, assistance, and data can be overwritten by supplied parameters.
265
+ Exception cause, assistance, and data can be overwritten by supplied parameters.
266
+
267
+ When we receive a common status code that we typically handle in the plugin, we raise an APIException
268
+ so that the status_code attribute can be easily accessed and raised further up the chain.
266
269
 
267
270
  :param response: Response object whose status should be checked.
268
271
  :type Response:
@@ -304,7 +307,7 @@ def response_handler(
304
307
  if hasattr(exception, "data") and data is not None:
305
308
  exception.data = data
306
309
  elif status_code_preset:
307
- exception = PluginException(preset=status_code_preset)
310
+ exception = APIException(preset=status_code_preset, status_code=status_code)
308
311
  exception.data = data
309
312
 
310
313
  raise exception
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: insightconnect-plugin-runtime
3
- Version: 6.3.0
3
+ Version: 6.3.2
4
4
  Summary: InsightConnect Plugin Runtime
5
5
  Home-page: https://github.com/rapid7/komand-plugin-sdk-python
6
6
  Author: Rapid7 Integrations Alliance
@@ -224,6 +224,8 @@ contributed. Black is installed as a test dependency and the hook can be initial
224
224
  after cloning this repository.
225
225
 
226
226
  ## Changelog
227
+ * 6.3.2 - Raise `APIException` from within the `response_handler` to easily access the status code within the plugin
228
+ * 6.3.1 - Improved filtering for `custom_config` parameters for plugin tasks
227
229
  * 6.3.0 - Add Tracing Instrumentation
228
230
  * 6.2.6 - Remove setuptools after installation
229
231
  * 6.2.5 - Fixed bug related to failure to set default region to assume role method in `aws_client` for newer versions of boto3 | Updated alpine image packages on build
@@ -4,7 +4,7 @@ insightconnect_plugin_runtime/cli.py,sha256=Pb-Janu-XfRlSXxPHh30OIquljWptrhhS51C
4
4
  insightconnect_plugin_runtime/connection.py,sha256=4bHHV2B0UFGsAtvLu1fiYQRwx7fissUakHPUyjLQO0E,2340
5
5
  insightconnect_plugin_runtime/dispatcher.py,sha256=ru7njnyyWE1-oD-VbZJ-Z8tELwvDf69rM7Iezs4rbnw,1774
6
6
  insightconnect_plugin_runtime/exceptions.py,sha256=Pvcdkx81o6qC2qU661x-DzNjuIMP82x52nPMSEqEo4s,8491
7
- insightconnect_plugin_runtime/helper.py,sha256=B0XqAXmn8CT1KQ6i5IoWLQrQ_HVOvuKrIKFuj_npQ-g,33770
7
+ insightconnect_plugin_runtime/helper.py,sha256=xGiskPd4vHr_k7BNEb3_7gPxm8sB85A7_5N31S-0RE4,33994
8
8
  insightconnect_plugin_runtime/metrics.py,sha256=hf_Aoufip_s4k4o8Gtzz90ymZthkaT2e5sXh5B4LcF0,3186
9
9
  insightconnect_plugin_runtime/plugin.py,sha256=Yf4LNczykDVc31F9G8uuJ9gxEsgmxmAr0n4pcZzichM,26393
10
10
  insightconnect_plugin_runtime/schema.py,sha256=6MVw5hqGATU1VLgwfOWfPsP3hy1OnsugCTsgX8sknes,521
@@ -16,7 +16,7 @@ insightconnect_plugin_runtime/trigger.py,sha256=Zq3cy68N3QxAGbNZKCID6CZF05Zi7YD2
16
16
  insightconnect_plugin_runtime/util.py,sha256=8cle29INhnshEcL2LWpaC0ZGqevjq8pW8TE0MFEiYYw,8475
17
17
  insightconnect_plugin_runtime/variables.py,sha256=7FjJGnU7KUR7m9o-_tRq7Q3KiaB1Pp0Apj1NGgOwrJk,3056
18
18
  insightconnect_plugin_runtime/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- insightconnect_plugin_runtime/api/endpoints.py,sha256=Kn9VfnpvcVO9oY5W07laJkXFNp0PfZQrY2AecYFbvNw,33377
19
+ insightconnect_plugin_runtime/api/endpoints.py,sha256=ddqqYM7s1huvXFD0nCjpV_J2XULDn8F5sRakdjq-AKU,38893
20
20
  insightconnect_plugin_runtime/api/schemas.py,sha256=jRmDrwLJTBl-iQOnyZkSwyJlCWg4eNjAnKfD9Eko4z0,2754
21
21
  insightconnect_plugin_runtime/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  insightconnect_plugin_runtime/clients/aws_client.py,sha256=ViJF3klbD1YM5GVxme3BFYfpuADUlVGP8sdbX2Z6_Cw,23061
@@ -67,7 +67,7 @@ tests/unit/test_action.py,sha256=0SVen1qNrFLKKAXMurN9T4NpItpftnSpDZ71AdrGZeo,168
67
67
  tests/unit/test_api.py,sha256=uZ1dWMmgQ-ZePYjmcZfjc-qOTJsjs20Wic0Uf4U12-8,4441
68
68
  tests/unit/test_aws_action.py,sha256=pBE23Qn4aXKJqPmwiHMcEU5zPdyvbKO-eK-6jUlrsQw,9640
69
69
  tests/unit/test_custom_encoder.py,sha256=KLYyVOTq9MEkZXyhVHqjm5LVSW6uJS4Davgghsw9DGk,2207
70
- tests/unit/test_endpoints.py,sha256=LuXOfLBu47rDjGa5YEsOwTZBEdvQdl_C6-r46oxWZA8,6401
70
+ tests/unit/test_endpoints.py,sha256=Ef0f6EudnAA_o6jZJ7rvgwgQHHswPuBEiyyo1D7BWMc,11523
71
71
  tests/unit/test_exceptions.py,sha256=Y4F-ij8WkEJkUU3mPvxlEchqE9NCdxDvR8bJzPVVNao,5328
72
72
  tests/unit/test_helpers.py,sha256=ym1tFi1VSKmdPaHEAlMEl1S7Ibu9-LrqZ2oqJv7bfbE,18685
73
73
  tests/unit/test_metrics.py,sha256=PjjTrB9w7uQ2Q5UN-893-SsH3EGJuBseOMHSD1I004s,7979
@@ -79,7 +79,7 @@ tests/unit/test_server_spec.py,sha256=je97BaktgK0Fiz3AwFPkcmHzYtOJJNqJV_Fw5hrvqX
79
79
  tests/unit/test_trigger.py,sha256=E53mAUoVyponWu_4IQZ0IC1gQ9lakBnTn_9vKN2IZfg,1692
80
80
  tests/unit/test_variables.py,sha256=OUEOqGYZA3Nd5oKk5GVY3hcrWKHpZpxysBJcO_v5gzs,291
81
81
  tests/unit/utils.py,sha256=hcY0A2H_DMgCDXUTvDtCXMdMvRjLQgTaGcTpATb8YG0,2236
82
- insightconnect_plugin_runtime-6.3.0.dist-info/METADATA,sha256=GCmcMcuLriTNb5PMLZc83UqlpxByddurw6G-WAwui-4,16225
83
- insightconnect_plugin_runtime-6.3.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
84
- insightconnect_plugin_runtime-6.3.0.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
85
- insightconnect_plugin_runtime-6.3.0.dist-info/RECORD,,
82
+ insightconnect_plugin_runtime-6.3.2.dist-info/METADATA,sha256=vgKGS09fSB4nQCCckH633xSzDf4UO6Waaae_XHnVivE,16419
83
+ insightconnect_plugin_runtime-6.3.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
84
+ insightconnect_plugin_runtime-6.3.2.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
85
+ insightconnect_plugin_runtime-6.3.2.dist-info/RECORD,,
@@ -1,11 +1,42 @@
1
- import unittest
2
1
  import json
2
+ import unittest
3
+ from typing import Any, Dict
3
4
 
4
- from insightconnect_plugin_runtime.api.endpoints import Endpoints
5
- from insightconnect_plugin_runtime.plugin import Plugin
6
- from insightconnect_plugin_runtime.action import Action
7
5
  from insightconnect_plugin_runtime import Input
6
+ from insightconnect_plugin_runtime.action import Action
7
+ from insightconnect_plugin_runtime.api.endpoints import Endpoints
8
8
  from insightconnect_plugin_runtime.connection import Connection
9
+ from insightconnect_plugin_runtime.plugin import Plugin
10
+ from insightconnect_plugin_runtime.task import Task
11
+ from parameterized import parameterized
12
+
13
+ MOCKED_CONFIG = {
14
+ "*": {
15
+ "first_property": "first_property_global",
16
+ "second_property": "second_property_global",
17
+ "task_name_1": {"second_property": "second_property_global_task_name_1"},
18
+ },
19
+ "org_1": {
20
+ "first_property": "first_property_org_1",
21
+ "11111111-1111-1111-1111-111111111111": {
22
+ "first_property": "first_property_org_1_11111111-1111-1111-1111-111111111111"
23
+ },
24
+ "task_name_2": {"second_property": "second_property_task_name_2"},
25
+ "task_name_3": {
26
+ "first_property": "first_property_task_name_3",
27
+ "second_property": "second_property_task_name_3",
28
+ },
29
+ "22222222-2222-2222-2222-222222222222": {
30
+ "first_property": "first_property_org_1_22222222-2222-2222-2222-222222222222",
31
+ "second_property": "second_property_org_1_22222222-2222-2222-2222-222222222222",
32
+ },
33
+ },
34
+ "org_2": {
35
+ "first_property": "first_property_org_2",
36
+ "second_property": "second_property_org_2",
37
+ "task_name_1": {"first_property": "first_property_org_1_task_name_1"},
38
+ },
39
+ }
9
40
 
10
41
 
11
42
  class TestDefinitionsAllActions(unittest.TestCase):
@@ -18,6 +49,7 @@ class TestDefinitionsAllActions(unittest.TestCase):
18
49
  workers=None,
19
50
  threads=None,
20
51
  master_pid=None,
52
+ config_options=MOCKED_CONFIG,
21
53
  )
22
54
 
23
55
  plugin = Plugin(
@@ -28,6 +60,11 @@ class TestDefinitionsAllActions(unittest.TestCase):
28
60
  connection=Connection(input=None),
29
61
  )
30
62
 
63
+ # Add example tasks
64
+ for task in ("task_name_1", "task_name_2", "task_name_3"):
65
+ plugin.add_task(
66
+ Task(name=task, description="Test", input=None, output=None)
67
+ )
31
68
  self.endpoints.plugin = plugin
32
69
 
33
70
  def test_input_good(self):
@@ -213,3 +250,106 @@ class TestDefinitionsAllActions(unittest.TestCase):
213
250
  actual = self.endpoints._create_action_definitions_payload()
214
251
 
215
252
  self.assertNotEqual(expected, actual)
253
+
254
+ @parameterized.expand(
255
+ [
256
+ (
257
+ "",
258
+ "",
259
+ "no_task_in_properties",
260
+ {
261
+ "first_property": "first_property_global",
262
+ "second_property": "second_property_global",
263
+ },
264
+ ),
265
+ (
266
+ "",
267
+ "",
268
+ "task_name_1",
269
+ {
270
+ "first_property": "first_property_global",
271
+ "second_property": "second_property_global_task_name_1",
272
+ },
273
+ ),
274
+ (
275
+ "org_1",
276
+ "",
277
+ "no_task_in_properties",
278
+ {
279
+ "first_property": "first_property_org_1",
280
+ "second_property": "second_property_global",
281
+ },
282
+ ),
283
+ (
284
+ "org_1",
285
+ "11111111-1111-1111-1111-111111111111",
286
+ "no_task_in_properties",
287
+ {
288
+ "first_property": "first_property_org_1_11111111-1111-1111-1111-111111111111",
289
+ "second_property": "second_property_global",
290
+ },
291
+ ),
292
+ (
293
+ "org_1",
294
+ "11111111-1111-1111-1111-111111111111",
295
+ "task_name_2",
296
+ {
297
+ "first_property": "first_property_org_1_11111111-1111-1111-1111-111111111111",
298
+ "second_property": "second_property_task_name_2",
299
+ },
300
+ ),
301
+ (
302
+ "org_1",
303
+ "11111111-1111-1111-1111-111111111111",
304
+ "task_name_3",
305
+ {
306
+ "first_property": "first_property_org_1_11111111-1111-1111-1111-111111111111",
307
+ "second_property": "second_property_task_name_3",
308
+ },
309
+ ),
310
+ (
311
+ "org_1",
312
+ "22222222-2222-2222-2222-222222222222",
313
+ "no_task_in_properties",
314
+ {
315
+ "first_property": "first_property_org_1_22222222-2222-2222-2222-222222222222",
316
+ "second_property": "second_property_org_1_22222222-2222-2222-2222-222222222222",
317
+ },
318
+ ),
319
+ (
320
+ "org_1",
321
+ "22222222-2222-2222-2222-222222222222",
322
+ "task_name_2",
323
+ {
324
+ "first_property": "first_property_org_1_22222222-2222-2222-2222-222222222222",
325
+ "second_property": "second_property_org_1_22222222-2222-2222-2222-222222222222",
326
+ },
327
+ ),
328
+ (
329
+ "org_2",
330
+ "",
331
+ "",
332
+ {
333
+ "first_property": "first_property_org_2",
334
+ "second_property": "second_property_org_2",
335
+ },
336
+ ),
337
+ (
338
+ "org_2",
339
+ "",
340
+ "task_name_1",
341
+ {
342
+ "first_property": "first_property_org_1_task_name_1",
343
+ "second_property": "second_property_org_2",
344
+ },
345
+ ),
346
+ ]
347
+ )
348
+ def test_add_plugin_custom_config(
349
+ self, org_id: str, int_id: str, task_name: str, expected: Dict[str, Any]
350
+ ) -> None:
351
+ response = self.endpoints.add_plugin_custom_config(
352
+ {"body": {}}, org_id, int_id, task_name
353
+ )
354
+ custom_config = response.get("body", {}).get("custom_config", {})
355
+ self.assertEqual(custom_config, expected)