1+ # Copyright 2025 Google LLC
2+ #
3+ # Licensed under the Apache License, Version 2.0 (the "License");
4+ # you may not use this file except in compliance with the License.
5+ # You may obtain a copy of the License at
6+ #
7+ # http://www.apache.org/licenses/LICENSE-2.0
8+ #
9+ # Unless required by applicable law or agreed to in writing, software
10+ # distributed under the License is distributed on an "AS IS" BASIS,
11+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ # See the License for the specific language governing permissions and
13+ # limitations under the License.
14+ #
15+ """Tests for format helper functions."""
16+ from __future__ import annotations
17+
18+ import pytest
19+
20+ from secops .chronicle .utils .format_utils import format_resource_id , parse_json_list
21+ from secops .exceptions import APIError
22+
23+
24+ def test_format_resource_id_returns_bare_id_unchanged () -> None :
25+ # A plain ID with no path prefix should pass through as-is
26+ assert format_resource_id ("123-ID-abc" ) == "123-ID-abc"
27+
28+
29+ def test_format_resource_id_extracts_id_from_full_resource_name () -> None :
30+ # Full resource name should have everything up to and including "projects/" stripped
31+ full_name = "projects/12345/locations/eu/instances/my-instance/nativeDashboards/123-ID-abc"
32+ assert format_resource_id (full_name ) == "12345/locations/eu/instances/my-instance/nativeDashboards/123-ID-abc"
33+
34+
35+ def test_format_resource_id_handles_minimal_projects_prefix () -> None :
36+ # Minimal case: just "projects/<id>"
37+ assert format_resource_id ("projects/my-project" ) == "my-project"
38+
39+
40+ def test_format_resource_id_does_not_alter_non_projects_paths () -> None :
41+ # Paths that don't start with "projects/" should be returned as-is
42+ assert format_resource_id ("instances/my-instance/dashboards/abc" ) == "instances/my-instance/dashboards/abc"
43+
44+
45+ def test_format_resource_id_empty_string_returns_empty_string () -> None :
46+ assert format_resource_id ("" ) == ""
47+
48+
49+ def test_parse_json_list_returns_list_unchanged () -> None :
50+ # A pre-built list should be returned as-is without any parsing
51+ value = [{"key" : "value" }, {"key2" : "value2" }]
52+ assert parse_json_list (value , "filters" ) is value
53+
54+
55+ def test_parse_json_list_parses_valid_json_array_string () -> None :
56+ json_str = '[{"key": "value"}, {"key2": "value2"}]'
57+ result = parse_json_list (json_str , "filters" )
58+ assert result == [{"key" : "value" }, {"key2" : "value2" }]
59+
60+
61+ def test_parse_json_list_wraps_single_json_object_in_list () -> None :
62+ # A JSON string containing a single object (not an array) should be wrapped
63+ json_str = '{"key": "value"}'
64+ result = parse_json_list (json_str , "filters" )
65+ assert result == [{"key" : "value" }]
66+
67+
68+ def test_parse_json_list_raises_api_error_on_invalid_json () -> None :
69+ with pytest .raises (APIError , match = "Invalid filters JSON" ):
70+ parse_json_list ("not valid json {" , "filters" )
71+
72+
73+ def test_parse_json_list_error_message_includes_field_name () -> None :
74+ # The field name should appear in the error to aid debugging
75+ with pytest .raises (APIError , match = "Invalid charts JSON" ):
76+ parse_json_list ("{bad json" , "charts" )
77+
78+
79+ def test_parse_json_list_raises_api_error_chained_from_value_error () -> None :
80+ # The APIError should chain from the underlying ValueError
81+ with pytest .raises (APIError ) as exc_info :
82+ parse_json_list ("bad json" , "filters" )
83+ assert exc_info .value .__cause__ is not None
84+ assert isinstance (exc_info .value .__cause__ , ValueError )
85+
86+
87+ def test_parse_json_list_handles_empty_json_array () -> None :
88+ result = parse_json_list ("[]" , "filters" )
89+ assert result == []
90+
91+
92+ def test_parse_json_list_handles_empty_list_input () -> None :
93+ result = parse_json_list ([], "filters" )
94+ assert result == []
0 commit comments