test_config.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import multiprocessing
  2. import os
  3. from pathlib import Path
  4. from typing import Any
  5. import pytest
  6. from pytest_mock import MockerFixture
  7. import reflex as rx
  8. import reflex.config
  9. from reflex.config import (
  10. EnvVar,
  11. env_var,
  12. environment,
  13. interpret_boolean_env,
  14. interpret_enum_env,
  15. interpret_int_env,
  16. )
  17. from reflex.constants import Endpoint, Env
  18. def test_requires_app_name():
  19. """Test that a config requires an app_name."""
  20. with pytest.raises(ValueError):
  21. rx.Config()
  22. def test_set_app_name(base_config_values):
  23. """Test that the app name is set to the value passed in.
  24. Args:
  25. base_config_values: Config values.
  26. """
  27. config = rx.Config(**base_config_values)
  28. assert config.app_name == base_config_values["app_name"]
  29. @pytest.mark.parametrize(
  30. "env_var, value",
  31. [
  32. ("APP_NAME", "my_test_app"),
  33. ("FRONTEND_PORT", 3001),
  34. ("FRONTEND_PATH", "/test"),
  35. ("BACKEND_PORT", 8001),
  36. ("API_URL", "https://mybackend.com:8000"),
  37. ("DEPLOY_URL", "https://myfrontend.com"),
  38. ("BACKEND_HOST", "127.0.0.1"),
  39. ("DB_URL", "postgresql://user:pass@localhost:5432/db"),
  40. ("REDIS_URL", "redis://localhost:6379"),
  41. ("TIMEOUT", 600),
  42. ("TELEMETRY_ENABLED", False),
  43. ("TELEMETRY_ENABLED", True),
  44. ],
  45. )
  46. def test_update_from_env(
  47. base_config_values: dict[str, Any],
  48. monkeypatch: pytest.MonkeyPatch,
  49. env_var: str,
  50. value: Any,
  51. ):
  52. """Test that environment variables override config values.
  53. Args:
  54. base_config_values: Config values.
  55. monkeypatch: The pytest monkeypatch object.
  56. env_var: The environment variable name.
  57. value: The environment variable value.
  58. """
  59. monkeypatch.setenv(env_var, str(value))
  60. assert os.environ.get(env_var) == str(value)
  61. config = rx.Config(**base_config_values)
  62. assert getattr(config, env_var.lower()) == value
  63. def test_update_from_env_path(
  64. base_config_values: dict[str, Any],
  65. monkeypatch: pytest.MonkeyPatch,
  66. tmp_path: Path,
  67. ):
  68. """Test that environment variables override config values.
  69. Args:
  70. base_config_values: Config values.
  71. monkeypatch: The pytest monkeypatch object.
  72. tmp_path: The pytest tmp_path fixture object.
  73. """
  74. monkeypatch.setenv("BUN_PATH", "/test")
  75. assert os.environ.get("BUN_PATH") == "/test"
  76. with pytest.raises(ValueError):
  77. rx.Config(**base_config_values)
  78. monkeypatch.setenv("BUN_PATH", str(tmp_path))
  79. assert os.environ.get("BUN_PATH") == str(tmp_path)
  80. config = rx.Config(**base_config_values)
  81. assert config.bun_path == tmp_path
  82. @pytest.mark.parametrize(
  83. "kwargs, expected",
  84. [
  85. (
  86. {"app_name": "test_app", "api_url": "http://example.com"},
  87. f"{Endpoint.EVENT}",
  88. ),
  89. (
  90. {"app_name": "test_app", "api_url": "http://example.com/api"},
  91. f"/api{Endpoint.EVENT}",
  92. ),
  93. ],
  94. )
  95. def test_event_namespace(mocker: MockerFixture, kwargs, expected):
  96. """Test the event namespace.
  97. Args:
  98. mocker: The pytest mock object.
  99. kwargs: The Config kwargs.
  100. expected: Expected namespace
  101. """
  102. conf = rx.Config(**kwargs)
  103. mocker.patch("reflex.config.get_config", return_value=conf)
  104. config = reflex.config.get_config()
  105. assert conf == config
  106. assert config.get_event_namespace() == expected
  107. DEFAULT_CONFIG = rx.Config(app_name="a")
  108. @pytest.mark.parametrize(
  109. ("config_kwargs", "env_vars", "set_persistent_vars", "exp_config_values"),
  110. [
  111. (
  112. {},
  113. {},
  114. {},
  115. {
  116. "api_url": DEFAULT_CONFIG.api_url,
  117. "backend_port": DEFAULT_CONFIG.backend_port,
  118. "deploy_url": DEFAULT_CONFIG.deploy_url,
  119. "frontend_port": DEFAULT_CONFIG.frontend_port,
  120. },
  121. ),
  122. # Ports set in config kwargs
  123. (
  124. {"backend_port": 8001, "frontend_port": 3001},
  125. {},
  126. {},
  127. {
  128. "api_url": "http://localhost:8001",
  129. "backend_port": 8001,
  130. "deploy_url": "http://localhost:3001",
  131. "frontend_port": 3001,
  132. },
  133. ),
  134. # Ports set in environment take precedence
  135. (
  136. {"backend_port": 8001, "frontend_port": 3001},
  137. {"BACKEND_PORT": 8002},
  138. {},
  139. {
  140. "api_url": "http://localhost:8002",
  141. "backend_port": 8002,
  142. "deploy_url": "http://localhost:3001",
  143. "frontend_port": 3001,
  144. },
  145. ),
  146. # Ports set on the command line take precedence
  147. (
  148. {"backend_port": 8001, "frontend_port": 3001},
  149. {"BACKEND_PORT": 8002},
  150. {"frontend_port": "3005"},
  151. {
  152. "api_url": "http://localhost:8002",
  153. "backend_port": 8002,
  154. "deploy_url": "http://localhost:3005",
  155. "frontend_port": 3005,
  156. },
  157. ),
  158. # api_url / deploy_url already set should not be overridden
  159. (
  160. {"api_url": "http://foo.bar:8900", "deploy_url": "http://foo.bar:3001"},
  161. {"BACKEND_PORT": 8002},
  162. {"frontend_port": "3005"},
  163. {
  164. "api_url": "http://foo.bar:8900",
  165. "backend_port": 8002,
  166. "deploy_url": "http://foo.bar:3001",
  167. "frontend_port": 3005,
  168. },
  169. ),
  170. ],
  171. )
  172. def test_replace_defaults(
  173. monkeypatch,
  174. config_kwargs,
  175. env_vars,
  176. set_persistent_vars,
  177. exp_config_values,
  178. ):
  179. """Test that the config replaces defaults with values from the environment.
  180. Args:
  181. monkeypatch: The pytest monkeypatch object.
  182. config_kwargs: The config kwargs.
  183. env_vars: The environment variables.
  184. set_persistent_vars: The values passed to config._set_persistent variables.
  185. exp_config_values: The expected config values.
  186. """
  187. mock_os_env = os.environ.copy()
  188. monkeypatch.setattr(reflex.config.os, "environ", mock_os_env)
  189. mock_os_env.update({k: str(v) for k, v in env_vars.items()})
  190. c = rx.Config(app_name="a", **config_kwargs)
  191. c._set_persistent(**set_persistent_vars)
  192. for key, value in exp_config_values.items():
  193. assert getattr(c, key) == value
  194. def reflex_dir_constant() -> Path:
  195. return environment.REFLEX_DIR.get()
  196. def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
  197. """Test that the REFLEX_DIR environment variable is used to set the Reflex.DIR constant.
  198. Args:
  199. monkeypatch: The pytest monkeypatch object.
  200. tmp_path: The pytest tmp_path object.
  201. """
  202. monkeypatch.setenv("REFLEX_DIR", str(tmp_path))
  203. mp_ctx = multiprocessing.get_context(method="spawn")
  204. assert reflex_dir_constant() == tmp_path
  205. with mp_ctx.Pool(processes=1) as pool:
  206. assert pool.apply(reflex_dir_constant) == tmp_path
  207. def test_interpret_enum_env() -> None:
  208. assert interpret_enum_env(Env.PROD.value, Env, "REFLEX_ENV") == Env.PROD
  209. def test_interpret_int_env() -> None:
  210. assert interpret_int_env("3001", "FRONTEND_PORT") == 3001
  211. @pytest.mark.parametrize("value, expected", [("true", True), ("false", False)])
  212. def test_interpret_bool_env(value: str, expected: bool) -> None:
  213. assert interpret_boolean_env(value, "TELEMETRY_ENABLED") == expected
  214. def test_env_var():
  215. class TestEnv:
  216. BLUBB: EnvVar[str] = env_var("default")
  217. INTERNAL: EnvVar[str] = env_var("default", internal=True)
  218. BOOLEAN: EnvVar[bool] = env_var(False)
  219. LIST: EnvVar[list[int]] = env_var([1, 2, 3])
  220. assert TestEnv.BLUBB.get() == "default"
  221. assert TestEnv.BLUBB.name == "BLUBB"
  222. TestEnv.BLUBB.set("new")
  223. assert os.environ.get("BLUBB") == "new"
  224. assert TestEnv.BLUBB.get() == "new"
  225. TestEnv.BLUBB.set(None)
  226. assert "BLUBB" not in os.environ
  227. assert TestEnv.INTERNAL.get() == "default"
  228. assert TestEnv.INTERNAL.name == "__INTERNAL"
  229. TestEnv.INTERNAL.set("new")
  230. assert os.environ.get("__INTERNAL") == "new"
  231. assert TestEnv.INTERNAL.get() == "new"
  232. assert TestEnv.INTERNAL.getenv() == "new"
  233. TestEnv.INTERNAL.set(None)
  234. assert "__INTERNAL" not in os.environ
  235. assert TestEnv.BOOLEAN.get() is False
  236. assert TestEnv.BOOLEAN.name == "BOOLEAN"
  237. TestEnv.BOOLEAN.set(True)
  238. assert os.environ.get("BOOLEAN") == "True"
  239. assert TestEnv.BOOLEAN.get() is True
  240. TestEnv.BOOLEAN.set(False)
  241. assert os.environ.get("BOOLEAN") == "False"
  242. assert TestEnv.BOOLEAN.get() is False
  243. TestEnv.BOOLEAN.set(None)
  244. assert "BOOLEAN" not in os.environ
  245. assert TestEnv.LIST.get() == [1, 2, 3]
  246. assert TestEnv.LIST.name == "LIST"
  247. TestEnv.LIST.set([4, 5, 6])
  248. assert os.environ.get("LIST") == "4:5:6"
  249. assert TestEnv.LIST.get() == [4, 5, 6]
  250. TestEnv.LIST.set(None)
  251. assert "LIST" not in os.environ
  252. @pytest.fixture
  253. def restore_env():
  254. """Fixture to restore the environment variables after the test.
  255. Yields:
  256. None: Placeholder for the test to run.
  257. """
  258. original_env = os.environ.copy()
  259. yield
  260. os.environ.clear()
  261. os.environ.update(original_env)
  262. @pytest.mark.usefixtures("restore_env")
  263. @pytest.mark.parametrize(
  264. ("file_map", "env_file", "exp_env_vars"),
  265. [
  266. (
  267. {
  268. ".env": "APP_NAME=my_test_app\nFRONTEND_PORT=3001\nBACKEND_PORT=8001\n",
  269. },
  270. "{path}/.env",
  271. {
  272. "APP_NAME": "my_test_app",
  273. "FRONTEND_PORT": "3001",
  274. "BACKEND_PORT": "8001",
  275. },
  276. ),
  277. (
  278. {
  279. ".env": "FRONTEND_PORT=4001",
  280. },
  281. "{path}/.env{sep}{path}/.env.local",
  282. {
  283. "FRONTEND_PORT": "4001",
  284. },
  285. ),
  286. (
  287. {
  288. ".env": "APP_NAME=my_test_app\nFRONTEND_PORT=3001\nBACKEND_PORT=8001\n",
  289. ".env.local": "FRONTEND_PORT=3002\n",
  290. },
  291. "{path}/.env.local{sep}{path}/.env",
  292. {
  293. "APP_NAME": "my_test_app",
  294. "FRONTEND_PORT": "3002", # Overrides .env
  295. "BACKEND_PORT": "8001",
  296. },
  297. ),
  298. ],
  299. )
  300. def test_env_file(
  301. tmp_path: Path,
  302. file_map: dict[str, str],
  303. env_file: str,
  304. exp_env_vars: dict[str, str],
  305. ) -> None:
  306. """Test that the env_file method loads environment variables from a file.
  307. Args:
  308. tmp_path: The pytest tmp_path object.
  309. file_map: A mapping of file names to their contents.
  310. env_file: The path to the environment file to load.
  311. exp_env_vars: The expected environment variables after loading the file.
  312. """
  313. for filename, content in file_map.items():
  314. (tmp_path / filename).write_text(content)
  315. _ = rx.Config(
  316. app_name="test_env_file",
  317. env_file=env_file.format(path=tmp_path, sep=os.pathsep),
  318. )
  319. for key, value in exp_env_vars.items():
  320. assert os.environ.get(key) == value