"""Unit tests for Variable class.""" from __future__ import annotations import pytest from cli.core.template.variable import Variable TEST_PORT = 8080 TEST_COUNT = 42 TEST_RATE = 3.14 TEST_SECRET_BYTES = 16 TEST_SLIDER_MAX = 9 TEST_SLIDER_STEP = 2 class TestVariableInitialization: """Tests for Variable initialization.""" def test_create_simple_variable(self): var = Variable({"name": "test_var", "type": "str"}) assert var.name == "test_var" assert var.type == "str" assert var.value is None def test_create_variable_with_default(self): var = Variable({"name": "port", "type": "int", "default": TEST_PORT}) assert var.name == "port" assert var.value == TEST_PORT def test_create_variable_with_value_takes_precedence_over_default(self): var = Variable({"name": "environment", "type": "str", "default": "prod", "value": "stage"}) assert var.value == "stage" def test_create_bool_variable_without_default(self): var = Variable({"name": "enabled", "type": "bool"}) assert var.value is False def test_create_variable_with_description(self): var = Variable({"name": "test", "type": "str", "description": "Test variable"}) assert var.description == "Test variable" def test_missing_name_raises_error(self): with pytest.raises(ValueError, match="must contain 'name' key"): Variable({"type": "str"}) def test_invalid_data_type_raises_error(self): with pytest.raises(ValueError, match="must be a dictionary"): Variable("not a dict") class TestVariableTypes: """Tests for variable type handling.""" def test_string_type(self): var = Variable({"name": "test", "type": "str", "default": "hello"}) assert var.value == "hello" def test_int_type(self): var = Variable({"name": "count", "type": "int", "default": TEST_COUNT}) assert var.value == TEST_COUNT def test_bool_type_true(self): var = Variable({"name": "enabled", "type": "bool", "default": True}) assert var.value is True def test_bool_type_false(self): var = Variable({"name": "disabled", "type": "bool", "default": False}) assert var.value is False def test_float_type(self): var = Variable({"name": "rate", "type": "float", "default": TEST_RATE}) assert var.value == TEST_RATE def test_autogenerated_is_rejected_for_non_secret_variables(self): with pytest.raises(ValueError, match="only supported for secret variables"): Variable({"name": "token", "type": "str", "config": {"autogenerated": True}}) class TestVariableProperties: """Tests for variable properties.""" def test_secret_autogenerated_flag_from_config(self): var = Variable({"name": "secret", "type": "secret", "config": {"autogenerated": True}}) assert var.autogenerated is True assert var.autogenerated_base64 is False def test_secret_autogenerated_base64_object(self): var = Variable( { "name": "secret", "type": "secret", "config": { "autogenerated": { "kind": "base64", "bytes": TEST_SECRET_BYTES, } }, } ) assert var.autogenerated is True assert var.autogenerated_base64 is True assert var.autogenerated_config is not None assert var.autogenerated_config.bytes == TEST_SECRET_BYTES def test_required_flag(self): var = Variable({"name": "hostname", "type": "str", "required": True}) assert var.required is True def test_options_list(self): var = Variable({"name": "mode", "type": "enum", "config": {"options": ["dev", "prod"]}}) assert var.options == ["dev", "prod"] def test_extra_help_text(self): var = Variable({"name": "test", "type": "str", "extra": "Additional info"}) assert var.extra == "Additional info" def test_string_textarea_and_placeholder_config(self): var = Variable( { "name": "notes", "type": "str", "config": { "textarea": True, "placeholder": "Line 1", }, } ) assert var.config.textarea is True assert var.config.placeholder == "Line 1" def test_integer_slider_config(self): var = Variable( { "name": "replicas", "type": "int", "default": 3, "config": { "slider": True, "min": 1, "max": TEST_SLIDER_MAX, "step": TEST_SLIDER_STEP, "unit": "nodes", }, } ) assert var.config.slider is True assert var.config.min == 1 assert var.config.max == TEST_SLIDER_MAX assert var.config.step == TEST_SLIDER_STEP assert var.config.unit == "nodes" class TestVariableNeeds: """Tests for variable dependency constraints.""" def test_needs_single_string(self): var = Variable({"name": "test", "type": "str", "needs": "other_var=value"}) assert var.needs == ["other_var=value"] def test_needs_semicolon_separated(self): var = Variable({"name": "test", "type": "str", "needs": "var1=value1;var2=value2"}) assert var.needs == ["var1=value1", "var2=value2"] def test_needs_list(self): var = Variable({"name": "test", "type": "str", "needs": ["var1=value1", "var2=value2"]}) assert var.needs == ["var1=value1", "var2=value2"] def test_needs_empty(self): var = Variable({"name": "test", "type": "str"}) assert var.needs == [] class TestVariableConversion: """Tests for variable value conversion.""" def test_convert_string_to_int(self): var = Variable({"name": "count", "type": "int"}) result = var.convert("42") assert result == TEST_COUNT assert isinstance(result, int) def test_convert_string_to_bool_true(self): var = Variable({"name": "enabled", "type": "bool"}) assert var.convert("true") is True assert var.convert("1") is True assert var.convert("yes") is True def test_convert_string_to_bool_false(self): var = Variable({"name": "disabled", "type": "bool"}) assert var.convert("false") is False assert var.convert("0") is False assert var.convert("no") is False def test_convert_string_to_float(self): var = Variable({"name": "rate", "type": "float"}) result = var.convert("3.14") assert result == TEST_RATE assert isinstance(result, float) def test_slider_validation_rejects_out_of_range_value(self): var = Variable( { "name": "replicas", "type": "int", "config": {"slider": True, "min": 1, "max": 5, "step": 2}, } ) with pytest.raises(ValueError, match="at most 5"): var.validate_and_convert(6) def test_slider_validation_rejects_step_mismatch(self): var = Variable( { "name": "replicas", "type": "int", "config": {"slider": True, "min": 1, "max": 7, "step": 2}, } ) with pytest.raises(ValueError, match="align with step 2"): var.validate_and_convert(4) def test_slider_default_value_is_validated(self): with pytest.raises(ValueError, match="at most 5"): Variable( { "name": "replicas", "type": "int", "default": 6, "config": {"slider": True, "min": 1, "max": 5}, } ) def test_autogenerated_secret_cannot_define_default(self): with pytest.raises(ValueError, match="autogenerated secrets cannot define defaults"): Variable( { "name": "secret", "type": "secret", "default": "fixed", "config": {"autogenerated": True}, } ) def test_base64_secret_cannot_use_length(self): with pytest.raises(ValueError, match="use bytes instead of length"): Variable( { "name": "secret", "type": "secret", "config": { "autogenerated": { "kind": "base64", "length": 16, } }, } ) def test_character_secret_rejects_multi_character_charset_entries(self): with pytest.raises(ValueError, match="exactly one character"): Variable( { "name": "secret", "type": "secret", "config": { "autogenerated": { "kind": "characters", "characters": ["ab"], } }, } ) @pytest.mark.parametrize( "var_type,default_value,expected", [ ("str", "hello", "hello"), ("int", TEST_COUNT, TEST_COUNT), ("bool", True, True), ("float", TEST_RATE, TEST_RATE), ], ) def test_variable_types_parametrized(var_type, default_value, expected): var = Variable({"name": "test", "type": var_type, "default": default_value}) assert var.value == expected