Add env() config binding with teleport() for CLI-to-worker config passing
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""Tests for env() object binding and teleport()."""
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Iterator
|
||||
|
||||
import msgspec
|
||||
import pytest
|
||||
from fastapi_vue import env, teleport
|
||||
from fastapi_vue.environ import PREFIX_VARIABLE, _Env
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_bindings(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
||||
env._bindings.clear() # noqa: SLF001
|
||||
monkeypatch.setenv(PREFIX_VARIABLE, "TEST")
|
||||
yield
|
||||
env._bindings.clear() # noqa: SLF001
|
||||
|
||||
|
||||
class StructConfig(msgspec.Struct):
|
||||
"""msgspec struct config."""
|
||||
|
||||
host: str = "localhost"
|
||||
port: int = 8000
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DataclassConfig:
|
||||
"""Dataclass config."""
|
||||
|
||||
host: str = "localhost"
|
||||
port: int = 8000
|
||||
|
||||
|
||||
def test_default_construction_and_identity() -> None:
|
||||
"""Unset env: default-constructed; repeated binds return the same object."""
|
||||
config = env(StructConfig)
|
||||
assert config.host == "localhost"
|
||||
assert env(StructConfig) is config
|
||||
|
||||
|
||||
def test_name_mangling() -> None:
|
||||
"""Class names uppercase as-is; repeated/edge underscores normalize."""
|
||||
env(StructConfig)
|
||||
teleport()
|
||||
assert json.loads(os.environ["TEST_STRUCTCONFIG"])["port"] == 8000
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _my__config_: # noqa: N801
|
||||
x: int = 0
|
||||
|
||||
env(_my__config_)
|
||||
teleport()
|
||||
assert "TEST_MY_CONFIG" in os.environ
|
||||
|
||||
|
||||
def test_name_override() -> None:
|
||||
"""name= overrides the mangled class name verbatim."""
|
||||
env(DataclassConfig, name="SETTINGS")
|
||||
teleport()
|
||||
assert "TEST_SETTINGS" in os.environ
|
||||
|
||||
|
||||
def test_name_conflict() -> None:
|
||||
"""A different type with a colliding env name raises KeyError."""
|
||||
env(StructConfig)
|
||||
with pytest.raises(KeyError, match="already bound"):
|
||||
env(DataclassConfig, name="STRUCTCONFIG")
|
||||
|
||||
|
||||
def test_reserved_names() -> None:
|
||||
"""Names used by fastapi-vue itself cannot be bound, by mangle or name=."""
|
||||
with pytest.raises(KeyError, match="reserved"):
|
||||
env(DataclassConfig, name="DEV")
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Dev:
|
||||
x: int = 0
|
||||
|
||||
with pytest.raises(KeyError, match="reserved"):
|
||||
env(Dev)
|
||||
|
||||
|
||||
def test_unsupported_type() -> None:
|
||||
"""Only dataclasses and msgspec.Structs can be bound."""
|
||||
with pytest.raises(TypeError, match=r"dataclass or msgspec\.Struct"):
|
||||
env(dict)
|
||||
|
||||
|
||||
def test_struct_teleport_and_decode() -> None:
|
||||
"""A mutated struct teleports; a fresh registry decodes it (worker view)."""
|
||||
config = env(StructConfig)
|
||||
config.port = 9000
|
||||
teleport()
|
||||
decoded = _Env()(StructConfig)
|
||||
assert decoded == config
|
||||
assert decoded is not config
|
||||
|
||||
|
||||
def test_dataclass_teleport_and_decode() -> None:
|
||||
"""Dataclasses round-trip through the environment via stdlib json."""
|
||||
config = env(DataclassConfig)
|
||||
config.host = "example.com"
|
||||
teleport()
|
||||
assert _Env()(DataclassConfig) == config
|
||||
|
||||
|
||||
def test_decode_on_bind_when_set(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""An already-set variable is decoded at binding time."""
|
||||
monkeypatch.setenv("TEST_STRUCTCONFIG", '{"host": "example.com", "port": 1}')
|
||||
assert env(StructConfig).host == "example.com"
|
||||
|
||||
|
||||
def test_teleport_without_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""teleport() with bindings but no FASTAPI_VUE prefix fails loudly."""
|
||||
monkeypatch.delenv(PREFIX_VARIABLE)
|
||||
env(StructConfig)
|
||||
with pytest.raises(RuntimeError, match=PREFIX_VARIABLE):
|
||||
teleport()
|
||||
|
||||
|
||||
def test_teleport_noop_without_bindings() -> None:
|
||||
"""No bindings: teleport() needs no prefix and does nothing."""
|
||||
teleport()
|
||||
Reference in New Issue
Block a user