# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Which .env keys a browser may write, and what makes an edit valid.""" from __future__ import annotations import pytest from signal_bot.groups import ConfigError from signal_bot.manage import envkeys SAMPLE = """\ PHONE_NUMBER=+15550000000 GROUP_IDS= COOLDOWN_SECONDS=10 """ def test_every_known_key_is_listed_with_whether_it_may_be_edited() -> None: listed = envkeys.settings(SAMPLE) by_key = {entry["key"]: entry for entry in listed} assert by_key["COOLDOWN_SECONDS"] == { "key": "COOLDOWN_SECONDS", "value": "10", "editable": True, } # Absent from the file is not absent from the page: empty is how .env # spells "use the default", and the page has to be able to show that. assert by_key["SILENCE_SECONDS"]["value"] == "" assert len(listed) == len(envkeys.EDITABLE) + len(envkeys.READ_ONLY) def test_the_account_and_the_container_it_talks_to_are_not_editable() -> None: # PHONE_NUMBER because the container holds the matching registration. # API_BASE_URL because repointing it sends every outgoing message and # every attachment fetch somewhere else on the next restart, which is a # strictly worse hole than changing the number. assert "PHONE_NUMBER" in envkeys.READ_ONLY assert "API_BASE_URL" in envkeys.READ_ONLY assert not set(envkeys.EDITABLE) & set(envkeys.READ_ONLY) def test_a_read_only_key_is_named_as_the_reason_for_refusing() -> None: assert envkeys.refuse({"PHONE_NUMBER": "+15550000001"}, groups_known=True) == "PHONE_NUMBER" def test_an_ordinary_edit_is_allowed() -> None: assert envkeys.refuse({"COOLDOWN_SECONDS": "20"}, groups_known=True) is None def test_group_keys_are_refused_while_the_container_is_unreachable() -> None: # parse_group_ids only checks syntax. Startup additionally requires that # every listed group is one the bot actually joined, judged against a # listing that was really read -- so writing one blind turns a scoping # mistake into a unit that will not start. assert envkeys.refuse({"GROUP_IDS": "group.x"}, groups_known=False) == "GROUP_IDS" assert envkeys.refuse({"SELFTEST_GROUPS": "x"}, groups_known=False) == "SELFTEST_GROUPS" def test_group_keys_are_allowed_once_a_listing_has_been_read() -> None: assert envkeys.refuse({"GROUP_IDS": "group.x"}, groups_known=True) is None def test_a_valid_edit_is_one_the_bot_could_start_on() -> None: envkeys.validate(SAMPLE, {"COOLDOWN_SECONDS": "20"}) def test_an_edit_the_bot_would_refuse_at_boot_is_refused_at_the_form() -> None: # The message is load_config's own, so the page shows what the journal # would have shown eight hours later. with pytest.raises(ConfigError, match="REPORT_HOUR"): envkeys.validate(SAMPLE, {"REPORT_HOUR": "99"}) def test_a_non_numeric_cooldown_is_refused() -> None: with pytest.raises(ConfigError, match="COOLDOWN_SECONDS"): envkeys.validate(SAMPLE, {"COOLDOWN_SECONDS": "soon"}) def test_an_unknown_log_level_is_refused() -> None: with pytest.raises(ConfigError, match="LOG_LEVEL"): envkeys.validate(SAMPLE, {"LOG_LEVEL": "LOUD"}) def test_a_trusted_inviter_that_is_neither_a_number_nor_a_uuid_is_refused() -> None: with pytest.raises(ConfigError, match="TRUSTED_INVITERS"): envkeys.validate(SAMPLE, {"TRUSTED_INVITERS": "whoever"})