def test_cli_compact_settings_envs( cli: typer.Typer, fake_settings_class: Type[BaseCustomSettings], fake_granular_env_file_content: str, export_as_dict: Callable, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch, ): with monkeypatch.context() as patch: mocked_envs_1: EnvVarsDict = setenvs_as_envfile( patch, fake_granular_env_file_content ) settings_1 = fake_settings_class() settings_1_dict_wo_secrets = export_as_dict(settings_1) assert settings_1_dict_wo_secrets == { "APP_HOST": "localhost", "APP_PORT": 80, "APP_OPTIONAL_ADDON": {"MODULE_VALUE": 10, "MODULE_VALUE_DEFAULT": 42}, "APP_REQUIRED_PLUGIN": { "POSTGRES_HOST": "localhost", "POSTGRES_PORT": 5432, "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "foodb", "POSTGRES_MINSIZE": 1, "POSTGRES_MAXSIZE": 50, "POSTGRES_CLIENT_NAME": None, }, } setting_env_content_compact = cli_runner.invoke( cli, ["settings", "--compact", "--show-secrets"], catch_exceptions=False, ).stdout # now we use these as env vars print(setting_env_content_compact) with monkeypatch.context() as patch: mocked_envs_2: EnvVarsDict = setenvs_as_envfile( patch, setting_env_content_compact, ) assert mocked_envs_2 == { "APP_HOST": "localhost", "APP_PORT": "80", "APP_OPTIONAL_ADDON": '{"MODULE_VALUE": 10, "MODULE_VALUE_DEFAULT": 42}', "APP_REQUIRED_PLUGIN": '{"POSTGRES_HOST": "localhost", "POSTGRES_PORT": 5432, "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "foodb", "POSTGRES_MINSIZE": 1, "POSTGRES_MAXSIZE": 50, "POSTGRES_CLIENT_NAME": null}', } settings_2 = fake_settings_class() assert settings_1 == settings_2
def test_granular_format(monkeypatch, fake_settings_class): setenvs_as_envfile( monkeypatch, """ APP_HOST=localhost APP_PORT=80 # --- APP_OPTIONAL_ADDON --- MODULE_VALUE=10 MODULE_VALUE_DEFAULT=42 # --- APP_REQUIRED_PLUGIN --- POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_USER=foo POSTGRES_PASSWORD=secret # Database name POSTGRES_DB=foodb # Minimum number of connections in the pool POSTGRES_MINSIZE=1 # Maximum number of connections in the pool POSTGRES_MAXSIZE=50 # Name of the application connecting the postgres database, will default to use the host hostname (hostname on linux) POSTGRES_CLIENT_NAME=None """, ) settings_from_envs = fake_settings_class() assert settings_from_envs == fake_settings_class( APP_HOST="localhost", APP_PORT=80, APP_OPTIONAL_ADDON={ "MODULE_VALUE": 10, "MODULE_VALUE_DEFAULT": 42 }, APP_REQUIRED_PLUGIN={ "POSTGRES_HOST": "localhost", "POSTGRES_PORT": 5432, "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "foodb", "POSTGRES_MINSIZE": 1, "POSTGRES_MAXSIZE": 50, "POSTGRES_CLIENT_NAME": None, }, )
def test_cli_default_settings_envs( cli: typer.Typer, fake_settings_class: Type[BaseCustomSettings], fake_granular_env_file_content: str, cli_runner: CliRunner, monkeypatch, ): with monkeypatch.context() as patch: mocked_envs_1: EnvVarsDict = setenvs_as_envfile( patch, fake_granular_env_file_content) cli_settings_output = cli_runner.invoke( cli, ["settings", "--show-secrets"], ).stdout # now let's use these as env vars print(cli_settings_output) with monkeypatch.context() as patch: mocked_envs_2: EnvVarsDict = setenvs_as_envfile( patch, cli_settings_output, ) settings_object = fake_settings_class() assert settings_object # NOTE: SEE BaseCustomSettings.Config.json_encoder for SecretStr settings_dict_wo_secrets = json.loads(settings_object.json(indent=2)) assert settings_dict_wo_secrets == { "APP_HOST": "localhost", "APP_PORT": 80, "APP_OPTIONAL_ADDON": { "MODULE_VALUE": 10, "MODULE_VALUE_DEFAULT": 42 }, "APP_REQUIRED_PLUGIN": { "POSTGRES_HOST": "localhost", "POSTGRES_PORT": 5432, "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "foodb", "POSTGRES_MINSIZE": 1, "POSTGRES_MAXSIZE": 50, "POSTGRES_CLIENT_NAME": None, }, }
def test_toggle_plugin_4(monkeypatch, model_classes_factory): *_, S4, S5 = model_classes_factory() JSON_VALUE = '{"POSTGRES_HOST":"pg2", "POSTGRES_USER":"******", "POSTGRES_PASSWORD":"******", "POSTGRES_DB":"db2"}' with monkeypatch.context() as patch: # Enables both setenvs_as_envfile( patch, f""" WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV='{JSON_VALUE}' WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL='{JSON_VALUE}' POSTGRES_HOST=pg POSTGRES_USER=test POSTGRES_PASSWORD=ssh POSTGRES_DB=db POSTGRES_CLIENT_NAME=name-is-now-set """, ) s4 = S4() s5 = S5() assert s4.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV is not None assert s5.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL is not None assert (s4.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV == s5.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL) with monkeypatch.context() as patch: # Enables both but remove individuals setenvs_as_envfile( patch, f""" WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV='{JSON_VALUE}' WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL='{JSON_VALUE}' """, ) s4 = S4() s5 = S5() assert s4.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV is not None assert s5.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL is not None assert (s4.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV == s5.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL)
def test_toggle_plugin_2(monkeypatch, model_classes_factory): *_, S4, S5 = model_classes_factory() # minimal setenvs_as_envfile( monkeypatch, """ POSTGRES_HOST=pg POSTGRES_USER=test POSTGRES_PASSWORD=ssh POSTGRES_DB=db """, ) s4 = S4() s5 = S5() # disabled by default assert s4.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV is not None assert s5.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL is None
def test_toggle_plugin_3(monkeypatch, model_classes_factory): *_, S4, S5 = model_classes_factory() # explicitly disables setenvs_as_envfile( monkeypatch, """ WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV=null POSTGRES_HOST=pg POSTGRES_USER=test POSTGRES_PASSWORD=ssh POSTGRES_DB=db """, ) s4 = S4() s5 = S5() assert s4.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV is None assert s5.WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL is None
def test_compact_format(monkeypatch, fake_settings_class): compact_envs: EnvVarsDict = setenvs_as_envfile( monkeypatch, """ APP_HOST=localhost APP_PORT=80 APP_OPTIONAL_ADDON='{"MODULE_VALUE": 10, "MODULE_VALUE_DEFAULT": 42}' APP_REQUIRED_PLUGIN='{"POSTGRES_HOST": "localhost", "POSTGRES_PORT": 5432, "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "foodb", "POSTGRES_MINSIZE": 1, "POSTGRES_MAXSIZE": 50, "POSTGRES_CLIENT_NAME": "None"}' """, ) settings_from_envs1 = fake_settings_class() settings_from_init = fake_settings_class(**envs_to_kwargs(compact_envs)) assert settings_from_envs1 == settings_from_init
def test_cli_settings_exclude_unset_as_json( cli: typer.Typer, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch, ): # minimal envfile mocked_envs: EnvVarsDict = setenvs_as_envfile( monkeypatch, """ # these are required APP_HOST=localhost APP_PORT=80 # --- APP_REQUIRED_PLUGIN --- # these are required POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_USER=foo POSTGRES_PASSWORD=secret POSTGRES_DB=foodb # this is optional but set POSTGRES_MAXSIZE=20 """, ) stdout_as_json = cli_runner.invoke( cli, ["settings", "--show-secrets", "--exclude-unset", "--as-json"], catch_exceptions=False, ).stdout print(stdout_as_json) # parsing output as json file envs_exclude_unset_from_json = json.loads(stdout_as_json) assert envs_exclude_unset_from_json == { "APP_HOST": "localhost", "APP_PORT": 80, "APP_REQUIRED_PLUGIN": { "POSTGRES_HOST": "localhost", "POSTGRES_PORT": 5432, "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "foodb", "POSTGRES_MAXSIZE": 20, }, }
def test_cli_settings_exclude_unset( cli: typer.Typer, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch, ): # minimal envfile mocked_envs: EnvVarsDict = setenvs_as_envfile( monkeypatch, """ # these are required APP_HOST=localhost APP_PORT=80 # --- APP_REQUIRED_PLUGIN --- # these are required POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_USER=foo POSTGRES_PASSWORD=secret POSTGRES_DB=foodb # this is optional but set POSTGRES_MAXSIZE=20 """, ) # using exclude-unset stdout_as_envfile = cli_runner.invoke( cli, ["settings", "--show-secrets", "--exclude-unset"], catch_exceptions=False, ).stdout print(stdout_as_envfile) # parsing output as an envfile envs_exclude_unset_from_env: EnvVarsDict = dotenv_values( stream=StringIO(stdout_as_envfile) ) assert envs_exclude_unset_from_env == mocked_envs
def test_parse_from_mixed_envs(monkeypatch, model_classes_factory): S1, S2, S3, S4, S5 = model_classes_factory() # environment # - Mixed with json (compact) AND postgres envs ENV_FILE = """ {0}='{{"POSTGRES_HOST":"pg2", "POSTGRES_USER":"******", "POSTGRES_PASSWORD":"******", "POSTGRES_DB":"db2"}}' POSTGRES_HOST=pg POSTGRES_USER=test POSTGRES_PASSWORD=ssh POSTGRES_DB=db POSTGRES_CLIENT_NAME=client-name """ with monkeypatch.context() as patch: setenvs_as_envfile( monkeypatch, ENV_FILE.format("WEBSERVER_POSTGRES"), ) s1 = S1() assert s1.dict() == { "WEBSERVER_POSTGRES": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PORT": 5432, "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", "POSTGRES_MAXSIZE": 50, "POSTGRES_MINSIZE": 1, "POSTGRES_CLIENT_NAME": "client-name", } } # NOTE how unset marks also applies to embedded fields # NOTE: (1) priority of json-compact over granulated # NOTE: (2) json-compact did not define this but granulated did assert s1.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES": { "POSTGRES_HOST": "pg2", # <- (1) "POSTGRES_USER": "******", # <- (1) "POSTGRES_PASSWORD": "******", # <- (1) "POSTGRES_DB": "db2", # <- (1) "POSTGRES_CLIENT_NAME": "client-name", # <- (2) } } with monkeypatch.context() as patch: setenvs_as_envfile( monkeypatch, ENV_FILE.format("WEBSERVER_POSTGRES_NULLABLE_OPTIONAL"), ) s2 = S2() assert s2.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES_NULLABLE_OPTIONAL": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", "POSTGRES_CLIENT_NAME": "client-name", } } with monkeypatch.context() as patch: setenvs_as_envfile( monkeypatch, ENV_FILE.format("WEBSERVER_POSTGRES_DEFAULT_ENV"), ) s3 = S3() assert s3.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES_DEFAULT_ENV": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", "POSTGRES_CLIENT_NAME": "client-name", } } with monkeypatch.context() as patch: setenvs_as_envfile( monkeypatch, ENV_FILE.format("WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV"), ) s4 = S4() assert s4.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", "POSTGRES_CLIENT_NAME": "client-name", } } with monkeypatch.context() as patch: setenvs_as_envfile( monkeypatch, ENV_FILE.format("WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL"), ) s5 = S5() assert s5.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", "POSTGRES_CLIENT_NAME": "client-name", } }
def test_parse_compact_env(monkeypatch, model_classes_factory): S1, S2, S3, S4, S5 = model_classes_factory() # environment # - with json (compact) JSON_VALUE = '{"POSTGRES_HOST":"pg2", "POSTGRES_USER":"******", "POSTGRES_PASSWORD":"******", "POSTGRES_DB":"db2"}' with monkeypatch.context() as patch: setenvs_as_envfile( patch, f""" WEBSERVER_POSTGRES='{JSON_VALUE}' """, ) # test s1 = S1() assert s1.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", } } assert s1.dict() == { "WEBSERVER_POSTGRES": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PORT": 5432, "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", "POSTGRES_MAXSIZE": 50, "POSTGRES_MINSIZE": 1, "POSTGRES_CLIENT_NAME": None, } } with monkeypatch.context() as patch: setenvs_as_envfile( patch, f""" WEBSERVER_POSTGRES_NULLABLE_OPTIONAL='{JSON_VALUE}' """, ) s2 = S2() assert s2.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES_NULLABLE_OPTIONAL": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", } } with monkeypatch.context() as patch: setenvs_as_envfile( patch, f""" WEBSERVER_POSTGRES_DEFAULT_ENV='{JSON_VALUE}' """, ) # NOTE: pydantic 1.9 does it right: it delays evaluating # default until it is really needed. Here before it would # fail because default cannot be computed even if the final value can! s3 = S3() assert s3.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES_DEFAULT_ENV": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", } } with monkeypatch.context() as patch: setenvs_as_envfile( patch, f""" WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV='{JSON_VALUE}' """, ) s4 = S4() assert s4.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", } } with monkeypatch.context() as patch: setenvs_as_envfile( patch, f""" WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL='{JSON_VALUE}' """, ) s5 = S5() assert s5.dict(exclude_unset=True) == { "WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL": { "POSTGRES_HOST": "pg2", "POSTGRES_USER": "******", "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db2", } }
def test_parse_from_individual_envs(monkeypatch, model_classes_factory): S1, S2, S3, S4, S5 = model_classes_factory() # environment # - with individual envs (PostgresSettings required fields) setenvs_as_envfile( monkeypatch, """ POSTGRES_HOST=pg POSTGRES_USER=test POSTGRES_PASSWORD=shh POSTGRES_DB=db """, ) with pytest.raises(ValidationError) as exc_info: S1() assert exc_info.value.errors()[0] == { "loc": ("WEBSERVER_POSTGRES", ), "msg": "field required", "type": "value_error.missing", } s2 = S2() assert s2.dict(exclude_unset=True) == {} assert s2.dict() == {"WEBSERVER_POSTGRES_NULLABLE_OPTIONAL": None} s3 = S3() assert s3.dict(exclude_unset=True) == {} assert s3.dict() == { "WEBSERVER_POSTGRES_DEFAULT_ENV": { "POSTGRES_HOST": "pg", "POSTGRES_USER": "******", "POSTGRES_PORT": 5432, "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db", "POSTGRES_MAXSIZE": 50, "POSTGRES_MINSIZE": 1, "POSTGRES_CLIENT_NAME": None, } } s4 = S4() assert s4.dict(exclude_unset=True) == {} assert s4.dict() == { "WEBSERVER_POSTGRES_NULLABLE_DEFAULT_ENV": { "POSTGRES_HOST": "pg", "POSTGRES_USER": "******", "POSTGRES_PORT": 5432, "POSTGRES_PASSWORD": "******", "POSTGRES_DB": "db", "POSTGRES_MAXSIZE": 50, "POSTGRES_MINSIZE": 1, "POSTGRES_CLIENT_NAME": None, } } s5 = S5() assert s5.dict(exclude_unset=True) == {} assert s5.dict() == {"WEBSERVER_POSTGRES_NULLABLE_DEFAULT_NULL": None}