test_variable.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. """Unit tests for Variable class."""
  2. from __future__ import annotations
  3. import pytest
  4. from cli.core.template.variable import Variable
  5. TEST_PORT = 8080
  6. TEST_COUNT = 42
  7. TEST_RATE = 3.14
  8. TEST_SECRET_BYTES = 16
  9. TEST_SLIDER_MAX = 9
  10. TEST_SLIDER_STEP = 2
  11. class TestVariableInitialization:
  12. """Tests for Variable initialization."""
  13. def test_create_simple_variable(self):
  14. var = Variable({"name": "test_var", "type": "str"})
  15. assert var.name == "test_var"
  16. assert var.type == "str"
  17. assert var.value is None
  18. def test_create_variable_with_default(self):
  19. var = Variable({"name": "port", "type": "int", "default": TEST_PORT})
  20. assert var.name == "port"
  21. assert var.value == TEST_PORT
  22. def test_create_variable_with_value_takes_precedence_over_default(self):
  23. var = Variable({"name": "environment", "type": "str", "default": "prod", "value": "stage"})
  24. assert var.value == "stage"
  25. def test_create_bool_variable_without_default(self):
  26. var = Variable({"name": "enabled", "type": "bool"})
  27. assert var.value is False
  28. def test_create_variable_with_description(self):
  29. var = Variable({"name": "test", "type": "str", "description": "Test variable"})
  30. assert var.description == "Test variable"
  31. def test_missing_name_raises_error(self):
  32. with pytest.raises(ValueError, match="must contain 'name' key"):
  33. Variable({"type": "str"})
  34. def test_invalid_data_type_raises_error(self):
  35. with pytest.raises(ValueError, match="must be a dictionary"):
  36. Variable("not a dict")
  37. class TestVariableTypes:
  38. """Tests for variable type handling."""
  39. def test_string_type(self):
  40. var = Variable({"name": "test", "type": "str", "default": "hello"})
  41. assert var.value == "hello"
  42. def test_int_type(self):
  43. var = Variable({"name": "count", "type": "int", "default": TEST_COUNT})
  44. assert var.value == TEST_COUNT
  45. def test_bool_type_true(self):
  46. var = Variable({"name": "enabled", "type": "bool", "default": True})
  47. assert var.value is True
  48. def test_bool_type_false(self):
  49. var = Variable({"name": "disabled", "type": "bool", "default": False})
  50. assert var.value is False
  51. def test_float_type(self):
  52. var = Variable({"name": "rate", "type": "float", "default": TEST_RATE})
  53. assert var.value == TEST_RATE
  54. def test_autogenerated_is_rejected_for_non_secret_variables(self):
  55. with pytest.raises(ValueError, match="only supported for secret variables"):
  56. Variable({"name": "token", "type": "str", "config": {"autogenerated": True}})
  57. class TestVariableProperties:
  58. """Tests for variable properties."""
  59. def test_secret_autogenerated_flag_from_config(self):
  60. var = Variable({"name": "secret", "type": "secret", "config": {"autogenerated": True}})
  61. assert var.autogenerated is True
  62. assert var.autogenerated_base64 is False
  63. def test_secret_autogenerated_base64_object(self):
  64. var = Variable(
  65. {
  66. "name": "secret",
  67. "type": "secret",
  68. "config": {
  69. "autogenerated": {
  70. "kind": "base64",
  71. "bytes": TEST_SECRET_BYTES,
  72. }
  73. },
  74. }
  75. )
  76. assert var.autogenerated is True
  77. assert var.autogenerated_base64 is True
  78. assert var.autogenerated_config is not None
  79. assert var.autogenerated_config.bytes == TEST_SECRET_BYTES
  80. def test_required_flag(self):
  81. var = Variable({"name": "hostname", "type": "str", "required": True})
  82. assert var.required is True
  83. def test_options_list(self):
  84. var = Variable({"name": "mode", "type": "enum", "config": {"options": ["dev", "prod"]}})
  85. assert var.options == ["dev", "prod"]
  86. def test_extra_help_text(self):
  87. var = Variable({"name": "test", "type": "str", "extra": "Additional info"})
  88. assert var.extra == "Additional info"
  89. def test_string_textarea_and_placeholder_config(self):
  90. var = Variable(
  91. {
  92. "name": "notes",
  93. "type": "str",
  94. "config": {
  95. "textarea": True,
  96. "placeholder": "Line 1",
  97. },
  98. }
  99. )
  100. assert var.config.textarea is True
  101. assert var.config.placeholder == "Line 1"
  102. def test_integer_slider_config(self):
  103. var = Variable(
  104. {
  105. "name": "replicas",
  106. "type": "int",
  107. "default": 3,
  108. "config": {
  109. "slider": True,
  110. "min": 1,
  111. "max": TEST_SLIDER_MAX,
  112. "step": TEST_SLIDER_STEP,
  113. "unit": "nodes",
  114. },
  115. }
  116. )
  117. assert var.config.slider is True
  118. assert var.config.min == 1
  119. assert var.config.max == TEST_SLIDER_MAX
  120. assert var.config.step == TEST_SLIDER_STEP
  121. assert var.config.unit == "nodes"
  122. class TestVariableNeeds:
  123. """Tests for variable dependency constraints."""
  124. def test_needs_single_string(self):
  125. var = Variable({"name": "test", "type": "str", "needs": "other_var=value"})
  126. assert var.needs == ["other_var=value"]
  127. def test_needs_semicolon_separated(self):
  128. var = Variable({"name": "test", "type": "str", "needs": "var1=value1;var2=value2"})
  129. assert var.needs == ["var1=value1", "var2=value2"]
  130. def test_needs_list(self):
  131. var = Variable({"name": "test", "type": "str", "needs": ["var1=value1", "var2=value2"]})
  132. assert var.needs == ["var1=value1", "var2=value2"]
  133. def test_needs_empty(self):
  134. var = Variable({"name": "test", "type": "str"})
  135. assert var.needs == []
  136. class TestVariableConversion:
  137. """Tests for variable value conversion."""
  138. def test_convert_string_to_int(self):
  139. var = Variable({"name": "count", "type": "int"})
  140. result = var.convert("42")
  141. assert result == TEST_COUNT
  142. assert isinstance(result, int)
  143. def test_convert_string_to_bool_true(self):
  144. var = Variable({"name": "enabled", "type": "bool"})
  145. assert var.convert("true") is True
  146. assert var.convert("1") is True
  147. assert var.convert("yes") is True
  148. def test_convert_string_to_bool_false(self):
  149. var = Variable({"name": "disabled", "type": "bool"})
  150. assert var.convert("false") is False
  151. assert var.convert("0") is False
  152. assert var.convert("no") is False
  153. def test_convert_string_to_float(self):
  154. var = Variable({"name": "rate", "type": "float"})
  155. result = var.convert("3.14")
  156. assert result == TEST_RATE
  157. assert isinstance(result, float)
  158. def test_slider_validation_rejects_out_of_range_value(self):
  159. var = Variable(
  160. {
  161. "name": "replicas",
  162. "type": "int",
  163. "config": {"slider": True, "min": 1, "max": 5, "step": 2},
  164. }
  165. )
  166. with pytest.raises(ValueError, match="at most 5"):
  167. var.validate_and_convert(6)
  168. def test_slider_validation_rejects_step_mismatch(self):
  169. var = Variable(
  170. {
  171. "name": "replicas",
  172. "type": "int",
  173. "config": {"slider": True, "min": 1, "max": 7, "step": 2},
  174. }
  175. )
  176. with pytest.raises(ValueError, match="align with step 2"):
  177. var.validate_and_convert(4)
  178. def test_slider_default_value_is_validated(self):
  179. with pytest.raises(ValueError, match="at most 5"):
  180. Variable(
  181. {
  182. "name": "replicas",
  183. "type": "int",
  184. "default": 6,
  185. "config": {"slider": True, "min": 1, "max": 5},
  186. }
  187. )
  188. def test_autogenerated_secret_cannot_define_default(self):
  189. with pytest.raises(ValueError, match="autogenerated secrets cannot define defaults"):
  190. Variable(
  191. {
  192. "name": "secret",
  193. "type": "secret",
  194. "default": "fixed",
  195. "config": {"autogenerated": True},
  196. }
  197. )
  198. def test_base64_secret_cannot_use_length(self):
  199. with pytest.raises(ValueError, match="use bytes instead of length"):
  200. Variable(
  201. {
  202. "name": "secret",
  203. "type": "secret",
  204. "config": {
  205. "autogenerated": {
  206. "kind": "base64",
  207. "length": 16,
  208. }
  209. },
  210. }
  211. )
  212. def test_character_secret_rejects_multi_character_charset_entries(self):
  213. with pytest.raises(ValueError, match="exactly one character"):
  214. Variable(
  215. {
  216. "name": "secret",
  217. "type": "secret",
  218. "config": {
  219. "autogenerated": {
  220. "kind": "characters",
  221. "characters": ["ab"],
  222. }
  223. },
  224. }
  225. )
  226. @pytest.mark.parametrize(
  227. "var_type,default_value,expected",
  228. [
  229. ("str", "hello", "hello"),
  230. ("int", TEST_COUNT, TEST_COUNT),
  231. ("bool", True, True),
  232. ("float", TEST_RATE, TEST_RATE),
  233. ],
  234. )
  235. def test_variable_types_parametrized(var_type, default_value, expected):
  236. var = Variable({"name": "test", "type": var_type, "default": default_value})
  237. assert var.value == expected