Skip to content

Commit f80f081

Browse files
committed
feat: validate config before saving to disk
- Added validation call in save_config to ensure config is valid before saving - Updated tests to verify validation behavior - Added test for empty description handling - Added test for schema validation errors
1 parent f109741 commit f80f081

File tree

10 files changed

+136
-48
lines changed

10 files changed

+136
-48
lines changed

helm_values_manager/cli.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Command line interface for the helm-values-manager plugin."""
22

3-
from typing import Optional
4-
53
import typer
64

75
from helm_values_manager.commands.add_deployment_command import AddDeploymentCommand
@@ -50,8 +48,8 @@ def init(
5048
@app.command("add-value-config")
5149
def add_value_config(
5250
path: str = typer.Option(..., "--path", "-p", help="Configuration path (e.g., 'app.replicas')"),
53-
description: Optional[str] = typer.Option(
54-
None, "--description", "-d", help="Description of what this configuration does"
51+
description: str = typer.Option(
52+
"Description of the configuration", "--description", "-d", help="Description of what this configuration does"
5553
),
5654
required: bool = typer.Option(False, "--required", "-r", help="Whether this configuration is required"),
5755
sensitive: bool = typer.Option(

helm_values_manager/commands/add_value_config_command.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional
44

55
from helm_values_manager.commands.base_command import BaseCommand
6+
from helm_values_manager.models.config_metadata import ConfigMetadata
67
from helm_values_manager.models.helm_values_config import HelmValuesConfig
78
from helm_values_manager.utils.logger import HelmLogger
89

@@ -35,9 +36,9 @@ def run(self, config: Optional[HelmValuesConfig] = None, **kwargs) -> str:
3536
if not path:
3637
raise ValueError("Path cannot be empty")
3738

38-
description = kwargs.get("description")
39-
required = kwargs.get("required", False)
40-
sensitive = kwargs.get("sensitive", False)
39+
description = kwargs.get("description", ConfigMetadata.DEFAULT_DESCRIPTION)
40+
required = kwargs.get("required", ConfigMetadata.DEFAULT_REQUIRED)
41+
sensitive = kwargs.get("sensitive", ConfigMetadata.DEFAULT_SENSITIVE)
4142

4243
try:
4344
# Add the new configuration path

helm_values_manager/commands/base_command.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ def save_config(self, config: HelmValuesConfig) -> None:
7373
7474
Raises:
7575
IOError: If unable to write to the file.
76+
ValueError: If the configuration is invalid.
7677
"""
78+
# Validate the config before saving
79+
config.validate()
80+
7781
try:
7882
with open(self.config_file, "w", encoding="utf-8") as f:
7983
json.dump(config.to_dict(), f, indent=2)
Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Simple dataclass for configuration metadata."""
22

33
from dataclasses import asdict, dataclass
4-
from typing import Any, Dict, Optional
4+
from typing import Any, ClassVar, Dict
55

66

77
@dataclass
@@ -10,14 +10,19 @@ class ConfigMetadata:
1010
Represents metadata for a configuration path.
1111
1212
Attributes:
13-
description (Optional[str]): Description of the configuration path.
13+
description (str): Description of the configuration path. Defaults to empty string.
1414
required (bool): Whether the configuration path is required. Defaults to False.
1515
sensitive (bool): Whether the configuration path is sensitive. Defaults to False.
1616
"""
1717

18-
description: Optional[str] = None
19-
required: bool = False
20-
sensitive: bool = False
18+
# Default values as class variables for reference elsewhere
19+
DEFAULT_DESCRIPTION: ClassVar[str] = ""
20+
DEFAULT_REQUIRED: ClassVar[bool] = False
21+
DEFAULT_SENSITIVE: ClassVar[bool] = False
22+
23+
description: str = DEFAULT_DESCRIPTION
24+
required: bool = DEFAULT_REQUIRED
25+
sensitive: bool = DEFAULT_SENSITIVE
2126

2227
def to_dict(self) -> Dict[str, Any]:
2328
"""Convert metadata to dictionary."""
@@ -27,7 +32,7 @@ def to_dict(self) -> Dict[str, Any]:
2732
def from_dict(cls, data: Dict[str, Any]) -> "ConfigMetadata":
2833
"""Create a ConfigMetadata instance from a dictionary."""
2934
return cls(
30-
description=data.get("description"),
31-
required=data.get("required", False),
32-
sensitive=data.get("sensitive", False),
35+
description=data.get("description", cls.DEFAULT_DESCRIPTION),
36+
required=data.get("required", cls.DEFAULT_REQUIRED),
37+
sensitive=data.get("sensitive", cls.DEFAULT_SENSITIVE),
3338
)

helm_values_manager/models/helm_values_config.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from jsonschema.exceptions import ValidationError
1010

1111
from helm_values_manager.backends.simple import SimpleValueBackend
12+
from helm_values_manager.models.config_metadata import ConfigMetadata
1213
from helm_values_manager.models.path_data import PathData
1314
from helm_values_manager.models.value import Value
1415
from helm_values_manager.utils.logger import HelmLogger
@@ -65,7 +66,11 @@ def _validate_schema(cls, data: dict) -> None:
6566
raise
6667

6768
def add_config_path(
68-
self, path: str, description: Optional[str] = None, required: bool = False, sensitive: bool = False
69+
self,
70+
path: str,
71+
description: str = ConfigMetadata.DEFAULT_DESCRIPTION,
72+
required: bool = ConfigMetadata.DEFAULT_REQUIRED,
73+
sensitive: bool = ConfigMetadata.DEFAULT_SENSITIVE,
6974
) -> None:
7075
"""
7176
Add a new configuration path.
@@ -191,9 +196,9 @@ def from_dict(cls, data: dict) -> "HelmValuesConfig":
191196
for config_item in data.get("config", []):
192197
path = config_item["path"]
193198
metadata = {
194-
"description": config_item.get("description"),
195-
"required": config_item.get("required", False),
196-
"sensitive": config_item.get("sensitive", False),
199+
"description": config_item.get("description", ConfigMetadata.DEFAULT_DESCRIPTION),
200+
"required": config_item.get("required", ConfigMetadata.DEFAULT_REQUIRED),
201+
"sensitive": config_item.get("sensitive", ConfigMetadata.DEFAULT_SENSITIVE),
197202
}
198203
path_data = PathData(path, metadata)
199204
config._path_map[path] = path_data

helm_values_manager/models/path_data.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
class PathData:
1616
"""Manages metadata and values for a configuration path."""
1717

18-
def __init__(self, path: str, metadata: Dict[str, Any]):
18+
def __init__(self, path: str, metadata: Dict[str, Any] = {}):
1919
"""
2020
Initialize PathData with a path and metadata.
2121
@@ -25,9 +25,9 @@ def __init__(self, path: str, metadata: Dict[str, Any]):
2525
"""
2626
self.path = path
2727
self._metadata = ConfigMetadata(
28-
description=metadata.get("description"),
29-
required=metadata.get("required", False),
30-
sensitive=metadata.get("sensitive", False),
28+
description=metadata.get("description", ConfigMetadata.DEFAULT_DESCRIPTION),
29+
required=metadata.get("required", ConfigMetadata.DEFAULT_REQUIRED),
30+
sensitive=metadata.get("sensitive", ConfigMetadata.DEFAULT_SENSITIVE),
3131
)
3232
self._values: Dict[str, Value] = {}
3333
HelmLogger.debug("Created PathData instance for path %s", path)
@@ -160,9 +160,9 @@ def from_dict(
160160
raise ValueError(f"Missing required keys: {missing}")
161161

162162
metadata = {
163-
"description": data.get("description"),
164-
"required": data.get("required", False),
165-
"sensitive": data.get("sensitive", False),
163+
"description": data.get("description", ConfigMetadata.DEFAULT_DESCRIPTION),
164+
"required": data.get("required", ConfigMetadata.DEFAULT_REQUIRED),
165+
"sensitive": data.get("sensitive", ConfigMetadata.DEFAULT_SENSITIVE),
166166
}
167167
path_data = cls(path=data["path"], metadata=metadata)
168168

tests/unit/commands/test_base_command.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,29 +159,89 @@ def test_execute_ensures_lock_release_on_error(base_command, valid_config):
159159

160160

161161
def test_save_config_success(base_command):
162-
"""Test successful config saving."""
162+
"""Test successful config save."""
163163
config = HelmValuesConfig()
164164
config.version = "1.0"
165165
config.release = "test"
166166

167+
# Mock the validate method
168+
config.validate = MagicMock()
169+
167170
with patch("builtins.open", mock_open()) as mock_file:
168171
base_command.save_config(config)
169172

173+
# Verify validate was called
174+
config.validate.assert_called_once()
175+
170176
mock_file.assert_called_once_with(base_command.config_file, "w", encoding="utf-8")
171177
handle = mock_file()
172178

179+
# Verify the written data
173180
written_json = "".join(call.args[0] for call in handle.write.call_args_list)
174181
written_data = json.loads(written_json)
175-
assert written_data["version"] == config.version
176-
assert written_data["release"] == config.release
182+
assert written_data["version"] == "1.0"
183+
assert written_data["release"] == "test"
177184

178185

179186
def test_save_config_io_error(base_command):
180-
"""Test save_config when IO error occurs."""
187+
"""Test IO error handling when saving config."""
181188
config = HelmValuesConfig()
182-
error_message = "Test error"
189+
config.version = "1.0"
190+
config.release = "test"
183191

184-
with patch("builtins.open", mock_open()) as mock_file:
185-
mock_file.return_value.write.side_effect = IOError(error_message)
186-
with pytest.raises(IOError, match=error_message):
192+
# Mock the validate method
193+
config.validate = MagicMock()
194+
195+
# Simulate an IO error
196+
mock_file = mock_open()
197+
mock_file.side_effect = IOError("Test IO Error")
198+
199+
with patch("builtins.open", mock_file):
200+
with pytest.raises(IOError) as excinfo:
187201
base_command.save_config(config)
202+
203+
assert "Test IO Error" in str(excinfo.value)
204+
205+
# Verify validate was called
206+
config.validate.assert_called_once()
207+
208+
209+
def test_save_config_validates_schema(base_command):
210+
"""Test that save_config validates the schema before saving."""
211+
# Create a mock config
212+
config = MagicMock(spec=HelmValuesConfig)
213+
214+
# Make validate method raise an error
215+
config.validate.side_effect = ValueError("Schema validation failed")
216+
217+
# Try to save the config
218+
with pytest.raises(ValueError, match="Schema validation failed"):
219+
base_command.save_config(config)
220+
221+
# Verify that validate was called
222+
config.validate.assert_called_once()
223+
224+
225+
def test_save_config_with_empty_description(base_command, tmp_path):
226+
"""Test that save_config handles empty description correctly."""
227+
# Create a real config
228+
config = HelmValuesConfig()
229+
config.release = "test"
230+
231+
# Add a config path with empty description (default)
232+
config.add_config_path("test.path")
233+
234+
# Set a temporary config file
235+
temp_file = tmp_path / "test_config.json"
236+
base_command.config_file = str(temp_file)
237+
238+
# Save the config
239+
base_command.save_config(config)
240+
241+
# Read the saved file
242+
with open(temp_file, "r") as f:
243+
data = json.load(f)
244+
245+
# Verify that description is an empty string
246+
assert data["config"][0]["description"] == ""
247+
assert isinstance(data["config"][0]["description"], str)

tests/unit/commands/test_init_command.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,20 @@ def test_initialization(init_command, tmp_path):
3636

3737
def test_run_creates_config(init_command):
3838
"""Test that run creates a new config with release name."""
39-
with patch("builtins.open", mock_open()) as mock_file:
40-
result = init_command.run(release_name="test-release")
41-
assert result == "Successfully initialized helm-values configuration."
42-
43-
# Verify config was saved with correct data
44-
handle = mock_file()
45-
written_json = "".join(call.args[0] for call in handle.write.call_args_list)
46-
written_data = json.loads(written_json)
47-
assert written_data["version"] == "1.0"
48-
assert written_data["release"] == "test-release"
49-
assert written_data["deployments"] == {}
50-
assert written_data["config"] == []
39+
# Mock the validate method
40+
with patch("helm_values_manager.models.helm_values_config.HelmValuesConfig.validate"):
41+
with patch("builtins.open", mock_open()) as mock_file:
42+
result = init_command.run(release_name="test-release")
43+
assert result == "Successfully initialized helm-values configuration."
44+
45+
# Verify config was saved with correct data
46+
handle = mock_file()
47+
written_json = "".join(call.args[0] for call in handle.write.call_args_list)
48+
written_data = json.loads(written_json)
49+
assert written_data["version"] == "1.0"
50+
assert written_data["release"] == "test-release"
51+
assert written_data["deployments"] == {}
52+
assert written_data["config"] == []
5153

5254

5355
def test_run_with_existing_config(init_command):

tests/unit/commands/test_set_value_command.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for the set-value command."""
22

33
import json
4-
from unittest.mock import mock_open, patch
4+
from unittest.mock import MagicMock, mock_open, patch
55

66
import pytest
77

@@ -133,3 +133,16 @@ def test_set_value_none_config(command):
133133
with patch.object(command, "load_config", return_value=None):
134134
with pytest.raises(ValueError, match="Configuration not loaded"):
135135
command.execute(path="app.replicas", environment="dev", value="3")
136+
137+
138+
def test_set_value_general_error():
139+
"""Test that general errors are properly handled."""
140+
command = SetValueCommand()
141+
config = MagicMock()
142+
config.deployments = {"dev": MagicMock()} # Mock deployment exists
143+
config.set_value.side_effect = Exception("Unexpected error")
144+
145+
with pytest.raises(Exception) as exc_info:
146+
command.run(config, path="app.replicas", environment="dev", value="3")
147+
148+
assert str(exc_info.value) == "Unexpected error"

tests/unit/models/test_schema_validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def test_default_values():
7272
}
7373
config = HelmValuesConfig.from_dict(config_data)
7474
path_data = config._path_map["app.config.key1"]
75-
assert path_data.metadata.description is None
75+
assert path_data.metadata.description == ""
7676
assert path_data.metadata.required is False
7777
assert path_data.metadata.sensitive is False
7878

0 commit comments

Comments
 (0)