| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- """Settings and smart plug secrets are write-only.
- Responses show a set secret as a fixed placeholder. Saving the placeholder back
- keeps the stored value, an empty value clears it, and a stored secret only goes
- to the destination it was entered for: changing the destination needs the
- secret entered again. Connection tests fill in a stored secret for those who
- may edit it, for its own destination only.
- """
- from __future__ import annotations
- from unittest.mock import AsyncMock, patch
- import pytest
- from httpx import AsyncClient
- from sqlalchemy import select
- from backend.app.models.settings import Settings
- from backend.app.models.smart_plug import SmartPlug
- from backend.app.services.secret_fields import SECRET_PLACEHOLDER
- pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
- M = SECRET_PLACEHOLDER
- SETTINGS_SECRETS = {
- "mqtt_password": "mqtt-pw",
- "ha_token": "ha-tok",
- "prometheus_token": "prom-tok",
- "obico_ml_token": "obico-tok",
- "virtual_printer_access_code": "12345678",
- }
- async def _admin(client: AsyncClient) -> dict[str, str]:
- await client.post(
- "/api/v1/auth/setup",
- json={"auth_enabled": True, "admin_username": "ssadmin", "admin_password": "AdminPass1!"},
- )
- login = await client.post("/api/v1/auth/login", json={"username": "ssadmin", "password": "AdminPass1!"})
- assert login.status_code == 200, login.text
- return {"Authorization": f"Bearer {login.json()['access_token']}"}
- async def _user(client: AsyncClient, admin: dict[str, str], name: str, permissions: list[str]) -> dict[str, str]:
- group = await client.post("/api/v1/groups/", headers=admin, json={"name": f"g_{name}", "permissions": permissions})
- assert group.status_code in (200, 201), group.text
- created = await client.post(
- "/api/v1/users/",
- headers=admin,
- json={"username": name, "password": "UserPass1!", "group_ids": [group.json()["id"]]},
- )
- assert created.status_code in (200, 201), created.text
- login = await client.post("/api/v1/auth/login", json={"username": name, "password": "UserPass1!"})
- return {"Authorization": f"Bearer {login.json()['access_token']}"}
- async def _stored_setting(db_session, key: str) -> str | None:
- db_session.expire_all()
- row = await db_session.scalar(select(Settings).where(Settings.key == key))
- return row.value if row else None
- async def _seed_settings(client: AsyncClient, admin: dict[str, str]) -> None:
- with patch("backend.app.services.mqtt_relay.mqtt_relay.configure", new_callable=AsyncMock):
- response = await client.put(
- "/api/v1/settings/",
- headers=admin,
- json={
- **SETTINGS_SECRETS,
- "mqtt_broker": "broker.example",
- "mqtt_port": 1883,
- "mqtt_use_tls": False,
- "ha_url": "http://ha.example:8123",
- "obico_ml_url": "http://obico.example:3333",
- },
- )
- assert response.status_code == 200, response.text
- async def _put_settings(client: AsyncClient, headers: dict[str, str], body: dict):
- with patch("backend.app.services.mqtt_relay.mqtt_relay.configure", new_callable=AsyncMock):
- return await client.put("/api/v1/settings/", headers=headers, json=body)
- # --- settings ---
- async def test_a_settings_reader_gets_placeholders(async_client: AsyncClient):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- viewer = await _user(async_client, admin, "sviewer", ["settings:read"])
- shown = (await async_client.get("/api/v1/settings/", headers=viewer)).json()
- saved_response = (await _put_settings(async_client, admin, {"date_format": "iso"})).json()
- for key in SETTINGS_SECRETS:
- assert shown[key] == M, key
- assert saved_response[key] == M, key
- async def test_an_unset_settings_secret_is_shown_empty(async_client: AsyncClient):
- admin = await _admin(async_client)
- shown = (await async_client.get("/api/v1/settings/", headers=admin)).json()
- assert shown["mqtt_password"] == ""
- async def test_settings_saved_back_as_shown_keep_their_secrets(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- shown = (await async_client.get("/api/v1/settings/", headers=admin)).json()
- response = await _put_settings(async_client, admin, shown)
- assert response.status_code == 200, response.text
- for key, value in SETTINGS_SECRETS.items():
- assert await _stored_setting(db_session, key) == value, key
- async def test_an_empty_settings_secret_clears_it(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- await _put_settings(async_client, admin, {"prometheus_token": ""})
- assert await _stored_setting(db_session, "prometheus_token") == ""
- @pytest.mark.parametrize(
- ("secret", "change"),
- [
- ("mqtt_password", {"mqtt_broker": "broker.elsewhere.example"}),
- ("mqtt_password", {"mqtt_port": 8883}),
- ("mqtt_password", {"mqtt_use_tls": True}),
- ("ha_token", {"ha_url": "http://ha.elsewhere.example:8123"}),
- ("obico_ml_token", {"obico_ml_url": "http://obico.elsewhere.example:3333"}),
- ],
- )
- @pytest.mark.parametrize("secret_sent", [True, False])
- async def test_a_kept_settings_secret_does_not_follow_a_new_destination(
- async_client: AsyncClient, db_session, secret, change, secret_sent
- ):
- """Kept means sent as the placeholder, or not sent at all."""
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- body = {**change, **({secret: M} if secret_sent else {})}
- response = await _put_settings(async_client, admin, body)
- assert response.status_code == 400, response.text
- field = next(iter(change))
- shown_before = {"mqtt_broker": "broker.example", "mqtt_port": "1883", "mqtt_use_tls": "false"}
- assert (
- await _stored_setting(db_session, field)
- == {
- **shown_before,
- "ha_url": "http://ha.example:8123",
- "obico_ml_url": "http://obico.example:3333",
- }[field]
- )
- async def test_a_new_settings_destination_with_its_secret_is_saved(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- response = await _put_settings(
- async_client, admin, {"ha_url": "http://ha.new.example:8123", "ha_token": "new-ha-tok"}
- )
- assert response.status_code == 200, response.text
- assert await _stored_setting(db_session, "ha_token") == "new-ha-tok"
- async def test_the_placeholder_cannot_stand_for_a_settings_secret_never_set(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- response = await _put_settings(async_client, admin, {"prometheus_token": M})
- assert response.status_code == 400, response.text
- assert not await _stored_setting(db_session, "prometheus_token")
- async def test_an_api_key_reader_still_gets_secrets_blank(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- key = await async_client.post("/api/v1/api-keys/", headers=admin, json={"name": "reader", "can_read_status": True})
- assert key.status_code in (200, 201), key.text
- api_key = key.json()["key"]
- shown = await async_client.get("/api/v1/settings/", headers={"X-API-Key": api_key})
- assert shown.status_code == 200, shown.text
- for name in SETTINGS_SECRETS:
- assert shown.json()[name] in ("", M), name
- # --- Home Assistant connection test ---
- async def _ha_test(client: AsyncClient, headers: dict[str, str], body: dict):
- with patch(
- "backend.app.api.routes.smart_plugs.homeassistant_service.test_connection",
- new_callable=AsyncMock,
- return_value={"success": True, "message": "ok", "error": None},
- ) as test:
- response = await client.post("/api/v1/smart-plugs/ha/test-connection", headers=headers, json=body)
- return response, test
- async def test_the_ha_test_uses_the_stored_token_for_the_stored_url(async_client: AsyncClient):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- response, test = await _ha_test(async_client, admin, {"url": "http://ha.example:8123", "token": M})
- assert response.status_code == 200, response.text
- test.assert_awaited_once_with("http://ha.example:8123", "ha-tok")
- async def test_the_ha_test_does_not_send_the_stored_token_elsewhere(async_client: AsyncClient):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- response, test = await _ha_test(async_client, admin, {"url": "http://ha.elsewhere.example:8123", "token": M})
- assert response.status_code == 400, response.text
- test.assert_not_awaited()
- async def test_the_ha_test_with_the_stored_token_needs_settings_update(async_client: AsyncClient):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- controller = await _user(async_client, admin, "plugctl", ["smart_plugs:control", "settings:read"])
- response, test = await _ha_test(async_client, controller, {"url": "http://ha.example:8123", "token": M})
- assert response.status_code == 403, response.text
- test.assert_not_awaited()
- # --- smart plugs ---
- TASMOTA = {"name": "T", "plug_type": "tasmota", "ip_address": "10.0.0.5", "username": "admin", "password": "tas-pw"}
- REST = {
- "name": "R",
- "plug_type": "rest",
- "rest_on_url": "http://plug.example/on",
- "rest_off_url": "http://plug.example/off",
- "rest_headers": '{"Authorization": "Bearer rest-tok"}',
- }
- async def _create_plug(client: AsyncClient, headers: dict[str, str], body: dict) -> dict:
- response = await client.post("/api/v1/smart-plugs/", headers=headers, json=body)
- assert response.status_code == 200, response.text
- return response.json()
- async def _stored_plug(db_session, plug_id: int) -> SmartPlug:
- db_session.expire_all()
- return await db_session.get(SmartPlug, plug_id)
- async def test_a_plug_reader_gets_placeholders(async_client: AsyncClient):
- admin = await _admin(async_client)
- viewer = await _user(async_client, admin, "pviewer", ["smart_plugs:read"])
- tasmota = await _create_plug(async_client, admin, TASMOTA)
- rest = await _create_plug(async_client, admin, REST)
- listed = {p["id"]: p for p in (await async_client.get("/api/v1/smart-plugs/", headers=viewer)).json()}
- single = (await async_client.get(f"/api/v1/smart-plugs/{tasmota['id']}", headers=viewer)).json()
- assert tasmota["password"] == M and rest["rest_headers"] == M
- assert listed[tasmota["id"]]["password"] == M
- assert listed[rest["id"]]["rest_headers"] == M
- assert single["password"] == M
- async def test_a_plug_saved_back_as_shown_keeps_its_secrets(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- tasmota = await _create_plug(async_client, admin, TASMOTA)
- rest = await _create_plug(async_client, admin, REST)
- for plug in (tasmota, rest):
- body = {
- k: plug[k]
- for k in (
- "name",
- "plug_type",
- "ip_address",
- "username",
- "password",
- "rest_on_url",
- "rest_off_url",
- "rest_headers",
- )
- }
- response = await async_client.patch(f"/api/v1/smart-plugs/{plug['id']}", headers=admin, json=body)
- assert response.status_code == 200, response.text
- assert (await _stored_plug(db_session, tasmota["id"])).password == "tas-pw"
- assert (await _stored_plug(db_session, rest["id"])).rest_headers == REST["rest_headers"]
- @pytest.mark.parametrize(
- ("plug", "change"),
- [
- (TASMOTA, {"ip_address": "10.0.0.66"}),
- (REST, {"rest_on_url": "http://elsewhere.example/on"}),
- (REST, {"rest_status_url": "http://elsewhere.example/status"}),
- (TASMOTA, {"plug_type": "rest", "rest_on_url": "http://elsewhere.example/on"}),
- ],
- )
- @pytest.mark.parametrize("secret_sent", [True, False])
- async def test_a_kept_plug_secret_does_not_follow_a_new_destination(
- async_client: AsyncClient, db_session, plug, change, secret_sent
- ):
- admin = await _admin(async_client)
- created = await _create_plug(async_client, admin, plug)
- secret = "password" if plug is TASMOTA else "rest_headers"
- body = {**change, **({secret: M} if secret_sent else {})}
- response = await async_client.patch(f"/api/v1/smart-plugs/{created['id']}", headers=admin, json=body)
- assert response.status_code == 400, response.text
- stored = await _stored_plug(db_session, created["id"])
- assert getattr(stored, secret) == plug[secret]
- assert stored.plug_type == plug["plug_type"]
- async def test_a_new_plug_destination_with_its_secret_is_saved(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- created = await _create_plug(async_client, admin, TASMOTA)
- response = await async_client.patch(
- f"/api/v1/smart-plugs/{created['id']}", headers=admin, json={"ip_address": "10.0.0.7", "password": "new-pw"}
- )
- assert response.status_code == 200, response.text
- assert (await _stored_plug(db_session, created["id"])).password == "new-pw"
- async def test_a_new_plug_cannot_take_the_placeholder_as_its_secret(async_client: AsyncClient):
- admin = await _admin(async_client)
- response = await async_client.post("/api/v1/smart-plugs/", headers=admin, json={**TASMOTA, "password": M})
- assert response.status_code == 400, response.text
- async def test_the_tasmota_test_uses_the_stored_password_for_its_own_ip(async_client: AsyncClient):
- admin = await _admin(async_client)
- created = await _create_plug(async_client, admin, TASMOTA)
- with patch(
- "backend.app.api.routes.smart_plugs.tasmota_service.test_connection",
- new_callable=AsyncMock,
- return_value={"success": True, "state": "ON"},
- ) as test:
- same = await async_client.post(
- "/api/v1/smart-plugs/test-connection",
- headers=admin,
- json={"ip_address": "10.0.0.5", "username": "admin", "password": M, "plug_id": created["id"]},
- )
- moved = await async_client.post(
- "/api/v1/smart-plugs/test-connection",
- headers=admin,
- json={"ip_address": "10.0.0.66", "username": "admin", "password": M, "plug_id": created["id"]},
- )
- assert same.status_code == 200, same.text
- test.assert_awaited_once_with("10.0.0.5", "admin", "tas-pw")
- assert moved.status_code == 400, moved.text
- async def test_a_plug_test_with_a_stored_secret_needs_plug_update(async_client: AsyncClient):
- admin = await _admin(async_client)
- controller = await _user(async_client, admin, "pctl", ["smart_plugs:read", "smart_plugs:control"])
- created = await _create_plug(async_client, admin, REST)
- with patch(
- "backend.app.api.routes.smart_plugs.rest_smart_plug_service.test_connection",
- new_callable=AsyncMock,
- return_value={"success": True},
- ) as test:
- response = await async_client.post(
- "/api/v1/smart-plugs/rest/test-connection",
- headers=controller,
- json={"url": "http://plug.example/on", "method": "POST", "headers": M, "plug_id": created["id"]},
- )
- assert response.status_code == 403, response.text
- test.assert_not_awaited()
- async def test_the_rest_test_uses_the_stored_headers_for_its_own_urls_only(async_client: AsyncClient):
- admin = await _admin(async_client)
- created = await _create_plug(async_client, admin, REST)
- with patch(
- "backend.app.api.routes.smart_plugs.rest_smart_plug_service.test_connection",
- new_callable=AsyncMock,
- return_value={"success": True},
- ) as test:
- same = await async_client.post(
- "/api/v1/smart-plugs/rest/test-connection",
- headers=admin,
- json={"url": "http://plug.example/off", "method": "POST", "headers": M, "plug_id": created["id"]},
- )
- moved = await async_client.post(
- "/api/v1/smart-plugs/rest/test-connection",
- headers=admin,
- json={"url": "http://elsewhere.example/x", "method": "POST", "headers": M, "plug_id": created["id"]},
- )
- assert same.status_code == 200, same.text
- test.assert_awaited_once_with("http://plug.example/off", "POST", REST["rest_headers"])
- assert moved.status_code == 400, moved.text
- async def test_a_test_without_a_plug_does_not_send_the_placeholder(async_client: AsyncClient):
- admin = await _admin(async_client)
- with patch("backend.app.api.routes.smart_plugs.tasmota_service.test_connection", new_callable=AsyncMock) as test:
- response = await async_client.post(
- "/api/v1/smart-plugs/test-connection",
- headers=admin,
- json={"ip_address": "10.0.0.5", "username": "admin", "password": M},
- )
- assert response.status_code == 400, response.text
- test.assert_not_awaited()
- async def test_a_plug_test_cannot_use_the_secret_of_a_plug_outside_ones_printers(
- async_client: AsyncClient, printer_factory
- ):
- admin = await _admin(async_client)
- mine, theirs = await printer_factory(name="Mine"), await printer_factory(name="Theirs")
- group = await async_client.post(
- "/api/v1/groups/",
- headers=admin,
- json={
- "name": "g_scoped",
- "permissions": ["smart_plugs:read", "smart_plugs:control", "smart_plugs:update"],
- "restrict_printers": True,
- "printer_ids": [mine.id],
- },
- )
- assert group.status_code in (200, 201), group.text
- await async_client.post(
- "/api/v1/users/",
- headers=admin,
- json={"username": "scoped", "password": "UserPass1!", "group_ids": [group.json()["id"]]},
- )
- login = await async_client.post("/api/v1/auth/login", json={"username": "scoped", "password": "UserPass1!"})
- scoped = {"Authorization": f"Bearer {login.json()['access_token']}"}
- created = await _create_plug(async_client, admin, {**TASMOTA, "printer_id": theirs.id})
- with patch("backend.app.api.routes.smart_plugs.tasmota_service.test_connection", new_callable=AsyncMock) as test:
- response = await async_client.post(
- "/api/v1/smart-plugs/test-connection",
- headers=scoped,
- json={"ip_address": "10.0.0.5", "username": "admin", "password": M, "plug_id": created["id"]},
- )
- assert response.status_code == 404, response.text
- test.assert_not_awaited()
- # --- Obico connection test ---
- async def _obico_test(client: AsyncClient, headers: dict[str, str], body: dict):
- with patch(
- "backend.app.api.routes.obico.obico_detection_service.test_connection",
- new_callable=AsyncMock,
- return_value={"ok": True, "status_code": 200, "body": "", "error": None, "auth_ok": True},
- ) as test:
- response = await client.post("/api/v1/obico/test-connection", headers=headers, json=body)
- return response, test
- @pytest.mark.parametrize("token", [M, None])
- async def test_the_obico_test_uses_the_stored_token_for_the_stored_url_only(async_client: AsyncClient, token):
- """The placeholder, or no token at all, means the stored token."""
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- body = {} if token is None else {"token": token}
- same, test = await _obico_test(async_client, admin, {"url": "http://obico.example:3333", **body})
- assert same.status_code == 200, same.text
- test.assert_awaited_once_with("http://obico.example:3333", "obico-tok")
- moved, test = await _obico_test(async_client, admin, {"url": "http://obico.elsewhere.example:3333", **body})
- assert moved.status_code == 400, moved.text
- test.assert_not_awaited()
- async def test_the_obico_test_with_a_typed_token_tests_that_token(async_client: AsyncClient):
- admin = await _admin(async_client)
- await _seed_settings(async_client, admin)
- response, test = await _obico_test(
- async_client, admin, {"url": "http://obico.elsewhere.example:3333", "token": "typed"}
- )
- assert response.status_code == 200, response.text
- test.assert_awaited_once_with("http://obico.elsewhere.example:3333", "typed")
- # --- round 2: env HA token, SMTP, LDAP, email security, REST header logs ---
- async def test_an_env_ha_token_lets_the_url_change_and_is_never_overwritten(
- async_client: AsyncClient, db_session, monkeypatch
- ):
- """A token from HA_TOKEN is the deployment's, not a stored secret to re-enter:
- the URL may change, and the placeholder sent back is not written anywhere."""
- admin = await _admin(async_client)
- monkeypatch.setenv("HA_TOKEN", "env-ha-tok")
- monkeypatch.delenv("HA_URL", raising=False)
- response = await _put_settings(async_client, admin, {"ha_url": "http://ha.new.example:8123", "ha_token": M})
- assert response.status_code == 200, response.text
- assert await _stored_setting(db_session, "ha_url") == "http://ha.new.example:8123"
- assert await _stored_setting(db_session, "ha_token") in (None, "")
- async def test_a_kept_smtp_password_does_not_follow_a_new_host(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- smtp = {
- "smtp_host": "smtp.example",
- "smtp_port": 587,
- "smtp_username": "u",
- "smtp_password": "smtp-pw",
- "smtp_security": "starttls",
- "smtp_auth_enabled": True,
- "smtp_from_email": "a@example.com",
- }
- assert (await async_client.post("/api/v1/auth/smtp", headers=admin, json=smtp)).status_code == 200
- for change in ({"smtp_host": "smtp.elsewhere.example"}, {"smtp_port": 25}, {"smtp_security": "none"}):
- moved = await async_client.post(
- "/api/v1/auth/smtp", headers=admin, json={**smtp, "smtp_password": None, **change}
- )
- assert moved.status_code == 400, (change, moved.text)
- assert await _stored_setting(db_session, "smtp_host") == "smtp.example"
- kept = await async_client.post("/api/v1/auth/smtp", headers=admin, json={**smtp, "smtp_password": None})
- assert kept.status_code == 200, kept.text
- assert await _stored_setting(db_session, "smtp_password") == "smtp-pw"
- entered = await async_client.post(
- "/api/v1/auth/smtp", headers=admin, json={**smtp, "smtp_host": "smtp.new.example", "smtp_password": "new-pw"}
- )
- assert entered.status_code == 200, entered.text
- assert await _stored_setting(db_session, "smtp_password") == "new-pw"
- async def test_a_new_smtp_host_without_authentication_drops_the_stored_password(async_client: AsyncClient, db_session):
- """Nothing to log in with now; the stored password must not be there for later."""
- admin = await _admin(async_client)
- smtp = {
- "smtp_host": "smtp.example",
- "smtp_port": 587,
- "smtp_username": "u",
- "smtp_password": "smtp-pw",
- "smtp_security": "starttls",
- "smtp_auth_enabled": True,
- "smtp_from_email": "a@example.com",
- }
- await async_client.post("/api/v1/auth/smtp", headers=admin, json=smtp)
- response = await async_client.post(
- "/api/v1/auth/smtp",
- headers=admin,
- json={**smtp, "smtp_host": "relay.example", "smtp_password": None, "smtp_auth_enabled": False},
- )
- assert response.status_code == 200, response.text
- assert not await _stored_setting(db_session, "smtp_password")
- async def test_a_kept_ldap_bind_password_does_not_follow_a_new_server(async_client: AsyncClient, db_session):
- admin = await _admin(async_client)
- await _put_settings(
- async_client,
- admin,
- {"ldap_server_url": "ldaps://ldap.example", "ldap_security": "ldaps", "ldap_bind_password": "bind-pw"},
- )
- for change in ({"ldap_server_url": "ldap://ldap.elsewhere.example"}, {"ldap_security": "starttls"}):
- response = await _put_settings(async_client, admin, change)
- assert response.status_code == 400, (change, response.text)
- assert await _stored_setting(db_session, "ldap_server_url") == "ldaps://ldap.example"
- response = await _put_settings(
- async_client, admin, {"ldap_server_url": "ldaps://ldap.new.example", "ldap_bind_password": "new-bind"}
- )
- assert response.status_code == 200, response.text
- assert await _stored_setting(db_session, "ldap_bind_password") == "new-bind"
- async def test_malformed_rest_headers_are_not_logged(caplog):
- from backend.app.services.rest_smart_plug import rest_smart_plug_service
- with caplog.at_level("DEBUG"):
- assert rest_smart_plug_service._parse_headers('{"Authorization": "Bearer leak-me",}') == {}
- assert "leak-me" not in caplog.text
- assert "REST headers" in caplog.text
|