test_github_restore_secret_destination.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. """A restore never sends a credential kept here to a place the backup names.
  2. Credentials are never restored, so the local one stays. The settings that say
  3. where it is sent (broker, port, TLS, URL) are left as they are when the backup
  4. would change them, as the settings editor refuses the same change without the
  5. credential being entered again.
  6. """
  7. import pytest
  8. from sqlalchemy import select
  9. from backend.app.models.settings import Settings
  10. from backend.app.schemas.github_backup import RestoreCategory
  11. from backend.app.services.github_restore import SETTINGS_PATH, GitHubRestoreService, _CategoryTally
  12. async def _local(db_session, rows: dict[str, str]) -> None:
  13. db_session.add_all(Settings(key=key, value=value) for key, value in rows.items())
  14. await db_session.commit()
  15. async def _restore(db_session, values: dict, overwrite: bool = True) -> tuple[_CategoryTally, set[str]]:
  16. tally, written = _CategoryTally(), set()
  17. await GitHubRestoreService()._restore_settings(
  18. db_session, {"settings": values}, overwrite=overwrite, tally=tally, keys_written=written
  19. )
  20. await db_session.commit()
  21. return tally, written
  22. async def _value(db_session, key: str) -> str | None:
  23. row = (await db_session.execute(select(Settings).where(Settings.key == key))).scalar_one_or_none()
  24. return row.value if row else None
  25. @pytest.mark.asyncio
  26. async def test_a_changed_broker_is_left_as_it_is_while_the_password_is_kept(db_session):
  27. await _local(db_session, {"mqtt_password": "local-value", "mqtt_broker": "here.local"})
  28. tally, written = await _restore(
  29. db_session, {"mqtt_broker": "elsewhere.example", "mqtt_password": "********", "currency": "EUR"}
  30. )
  31. assert await _value(db_session, "mqtt_broker") == "here.local"
  32. assert await _value(db_session, "mqtt_password") == "local-value"
  33. assert written == {"currency"}
  34. kept = [note for note in tally.notes if note["code"] == "settingsDestinationKept"]
  35. assert kept and kept[0]["params"]["keys"] == "mqtt_broker"
  36. @pytest.mark.asyncio
  37. async def test_without_a_stored_credential_the_destination_is_restored(db_session):
  38. await _local(db_session, {"mqtt_broker": "here.local", "mqtt_password": " "})
  39. _, written = await _restore(db_session, {"mqtt_broker": "elsewhere.example"})
  40. assert await _value(db_session, "mqtt_broker") == "elsewhere.example"
  41. assert written == {"mqtt_broker"}
  42. @pytest.mark.asyncio
  43. async def test_an_unchanged_destination_is_restored_as_usual(db_session):
  44. await _local(db_session, {"mqtt_password": "local-value", "mqtt_broker": "here.local", "mqtt_use_tls": "false"})
  45. tally, written = await _restore(
  46. db_session, {"mqtt_broker": "here.local", "mqtt_port": "1883", "mqtt_use_tls": False}
  47. )
  48. assert written == {"mqtt_broker", "mqtt_port", "mqtt_use_tls"}
  49. assert "settingsDestinationKept" not in [note["code"] for note in tally.notes]
  50. @pytest.mark.asyncio
  51. @pytest.mark.parametrize(
  52. ("key", "value"),
  53. [("mqtt_port", "8883"), ("mqtt_use_tls", "true"), ("mqtt_use_tls", True)],
  54. )
  55. async def test_a_destination_never_saved_here_counts_from_its_default(db_session, key, value):
  56. await _local(db_session, {"mqtt_password": "local-value"})
  57. _, written = await _restore(db_session, {key: value})
  58. assert await _value(db_session, key) is None
  59. assert written == set()
  60. @pytest.mark.asyncio
  61. @pytest.mark.parametrize(
  62. ("secret", "destination"),
  63. [("ha_token", "ha_url"), ("obico_ml_token", "obico_ml_url")],
  64. )
  65. async def test_a_token_does_not_follow_a_changed_url(db_session, monkeypatch, secret, destination):
  66. monkeypatch.delenv("HA_TOKEN", raising=False)
  67. await _local(db_session, {secret: "local-value", destination: "http://here.local"})
  68. _, written = await _restore(db_session, {destination: "http://elsewhere.example"})
  69. assert await _value(db_session, destination) == "http://here.local"
  70. assert written == set()
  71. @pytest.mark.asyncio
  72. async def test_a_deployment_token_may_follow_a_changed_url(db_session, monkeypatch):
  73. """Set in the environment, the token is the deployment's own: as in the editor."""
  74. monkeypatch.setenv("HA_TOKEN", "env-value")
  75. await _local(db_session, {"ha_token": "local-value", "ha_url": "http://here.local"})
  76. await _restore(db_session, {"ha_url": "http://elsewhere.example"})
  77. assert await _value(db_session, "ha_url") == "http://elsewhere.example"
  78. @pytest.mark.asyncio
  79. async def test_the_preview_counts_and_names_what_is_left_as_it_is(db_session):
  80. await _local(db_session, {"mqtt_password": "local-value", "mqtt_broker": "here.local"})
  81. parsed = {SETTINGS_PATH: {"settings": {"mqtt_broker": "elsewhere.example", "currency": "EUR"}}}
  82. count, detail = await GitHubRestoreService()._count_items(db_session, RestoreCategory.SETTINGS, parsed)
  83. assert count == 1
  84. assert detail is not None and detail.code == "settingsDestinationWillKeep"
  85. assert detail.params == {"destination": 1}
  86. @pytest.mark.asyncio
  87. @pytest.mark.parametrize(
  88. ("key", "local", "restored"),
  89. [
  90. ("smtp_host", "mail.here.local", "mail.elsewhere.example"),
  91. ("smtp_port", "587", "25"),
  92. ("smtp_security", "starttls", "none"),
  93. ("smtp_use_tls", "true", "false"),
  94. ],
  95. )
  96. async def test_the_smtp_password_does_not_follow_a_changed_server(db_session, key, local, restored):
  97. await _local(db_session, {"smtp_password": "local-value", key: local})
  98. _, written = await _restore(db_session, {key: restored})
  99. assert await _value(db_session, key) == local
  100. assert written == set()
  101. @pytest.mark.asyncio
  102. async def test_a_destination_without_a_row_here_is_left_alone_while_a_credential_is_kept(db_session):
  103. """Without a row there is no value to compare with: counted as a change."""
  104. await _local(db_session, {"smtp_password": "local-value", "smtp_host": "mail.here.local"})
  105. _, written = await _restore(db_session, {"smtp_security": "none"})
  106. assert await _value(db_session, "smtp_security") is None
  107. assert written == set()
  108. @pytest.mark.asyncio
  109. async def test_the_manyfold_secret_does_not_follow_a_changed_url(db_session):
  110. await _local(db_session, {"manyfold_client_secret": "local-value", "manyfold_url": "http://here.local"})
  111. _, written = await _restore(db_session, {"manyfold_url": "http://elsewhere.example", "manyfold_client_id": "id"})
  112. assert await _value(db_session, "manyfold_url") == "http://here.local"
  113. assert written == {"manyfold_client_id"}
  114. @pytest.mark.asyncio
  115. async def test_without_a_stored_smtp_password_the_server_is_restored(db_session):
  116. await _local(db_session, {"smtp_host": "mail.here.local"})
  117. _, written = await _restore(db_session, {"smtp_host": "mail.elsewhere.example", "smtp_security": "ssl"})
  118. assert await _value(db_session, "smtp_host") == "mail.elsewhere.example"
  119. assert written == {"smtp_host", "smtp_security"}