Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom_components/lock_code_manager/providers/akuvox.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
)
from ..domain.models import SlotCredential
from ._base import BaseLock
from ._util import make_legacy_tagged_name as _make_tagged_name, parse_tag as _parse_tag
from ._util import make_tagged_name as _make_tagged_name, parse_tag as _parse_tag
from .const import LOGGER
Comment on lines 29 to 32

AKUVOX_DOMAIN = "local_akuvox"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/lock_code_manager/providers/schlage.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
)
from ..domain.models import SlotCredential
from ._base import BaseLock
from ._util import make_legacy_tagged_name as _make_tagged_name, parse_tag as _parse_tag
from ._util import make_tagged_name as _make_tagged_name, parse_tag as _parse_tag
from .const import LOGGER
Comment on lines 36 to 38

SCHLAGE_DOMAIN = "schlage"
Expand Down
10 changes: 5 additions & 5 deletions tests/providers/akuvox/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async def test_set_credential_existing(
akuvox_mock_services["list_users"] = AsyncMock(
return_value={
entity_id: {
"users": [make_user("100", "[LCM:1] Existing", "1234")],
"users": [make_user("100", "lcm:1:Existing", "1234")],
},
}
)
Expand Down Expand Up @@ -117,7 +117,7 @@ async def test_delete_credential(
akuvox_mock_services["list_users"] = AsyncMock(
return_value={
entity_id: {
"users": [make_user("100", "[LCM:1] Guest", "1234")],
"users": [make_user("100", "lcm:1:Guest", "1234")],
},
}
)
Expand Down Expand Up @@ -159,7 +159,7 @@ async def test_base_orchestration_clear_credential(
akuvox_mock_services["list_users"] = AsyncMock(
return_value={
entity_id: {
"users": [make_user("100", "[LCM:1] Guest", "1234")],
"users": [make_user("100", "lcm:1:Guest", "1234")],
},
}
)
Expand Down Expand Up @@ -196,7 +196,7 @@ async def test_get_users_returns_codes(
akuvox_mock_services["list_users"] = AsyncMock(
return_value={
entity_id: {
"users": [make_user("100", "[LCM:1] Guest", "4321")],
"users": [make_user("100", "lcm:1:Guest", "4321")],
},
}
)
Expand All @@ -223,7 +223,7 @@ async def test_get_usercodes_projection(
akuvox_mock_services["list_users"] = AsyncMock(
return_value={
entity_id: {
"users": [make_user("100", "[LCM:1] Guest", "4321")],
"users": [make_user("100", "lcm:1:Guest", "4321")],
},
}
)
Expand Down
71 changes: 58 additions & 13 deletions tests/providers/akuvox/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ class TestHelperFunctions:
@pytest.mark.parametrize(
("slot", "name", "expected"),
[
pytest.param(1, "Guest", "[LCM:1] Guest", id="with-name"),
pytest.param(5, None, "[LCM:5] Code Slot 5", id="without-name"),
pytest.param(3, None, "[LCM:3] Code Slot 3", id="none-name"),
pytest.param(1, "Guest", "lcm:1:Guest", id="with-name"),
pytest.param(5, None, "lcm:5:Code Slot 5", id="without-name"),
pytest.param(3, None, "lcm:3:Code Slot 3", id="none-name"),
],
)
def test_make_tagged_name(self, slot: int, name: str | None, expected: str) -> None:
Expand Down Expand Up @@ -275,7 +275,7 @@ async def test_get_users_tagged_user_no_pin(
mock_response = {
LOCK_ENTITY_ID: {
"users": [
make_user("400", "[LCM:1] Empty Slot", ""),
make_user("400", "lcm:1:Empty Slot", ""),
],
},
}
Expand All @@ -297,7 +297,7 @@ async def test_get_users_tagged_outside_managed_range(
mock_response = {
LOCK_ENTITY_ID: {
"users": [
make_user("500", "[LCM:99] Outside", "5555"),
make_user("500", "lcm:99:Outside", "5555"),
],
},
}
Expand All @@ -322,7 +322,7 @@ async def test_get_users_known_pin(
mock_response = {
LOCK_ENTITY_ID: {
"users": [
make_user("100", "[LCM:1] Guest", "4321"),
make_user("100", "lcm:1:Guest", "4321"),
],
},
}
Expand All @@ -348,6 +348,50 @@ async def test_set_credential_no_name_keeps_existing(
self, hass: HomeAssistant, akuvox_lock: AkuvoxLock
) -> None:
"""Test setting a credential without a name preserves the existing name."""
list_response = {
LOCK_ENTITY_ID: {
"users": [
make_user("100", "lcm:1:Guest", "1234"),
],
},
}
register_mock_service(
hass, AKUVOX_DOMAIN, "list_users", AsyncMock(return_value=list_response)
)

modify_calls: list[dict[str, Any]] = []

async def _capture_modify(call):
modify_calls.append(dict(call.data))

register_mock_service(
hass, AKUVOX_DOMAIN, "modify_user", AsyncMock(side_effect=_capture_modify)
)

result = await akuvox_lock.async_set_credential(
1,
_pin_cred(1, "9999"),
"9999",
name=None,
source="direct",
)

assert result is True
assert modify_calls[0]["name"] == "lcm:1:Guest"

async def test_set_credential_migrates_legacy_format_tag_on_write(
self, hass: HomeAssistant, akuvox_lock: AkuvoxLock
) -> None:
"""
Touching a legacy ``[LCM:<slot>]``-tagged user rewrites it to canonical.

Pre-PR-C installs have users named ``[LCM:1] Guest``. The
tolerant parser (added in #1238) discovers them by slot, so
the provider finds the user at slot 1; the rewrite happens
implicitly because ``_make_tagged_name`` now produces the
canonical format. The next modify_user call carries the
canonical name, completing the per-user migration.
"""
list_response = {
LOCK_ENTITY_ID: {
"users": [
Expand Down Expand Up @@ -377,7 +421,8 @@ async def _capture_modify(call):
)

assert result is True
assert modify_calls[0]["name"] == "[LCM:1] Guest"
# Friendly portion preserved verbatim; only the format changed.
assert modify_calls[0]["name"] == "lcm:1:Guest"

async def test_set_credential_service_failure(
self, hass: HomeAssistant, akuvox_lock: AkuvoxLock
Expand Down Expand Up @@ -431,7 +476,7 @@ async def test_delete_credential_service_failure(
list_response = {
LOCK_ENTITY_ID: {
"users": [
make_user("100", "[LCM:1] Guest", "1234"),
make_user("100", "lcm:1:Guest", "1234"),
],
},
}
Expand Down Expand Up @@ -557,7 +602,7 @@ async def _capture_modify(call):

assert len(modify_calls) == 1
assert modify_calls[0]["id"] == "200"
assert modify_calls[0]["name"] == "[LCM:1] Visitor"
assert modify_calls[0]["name"] == "lcm:1:Visitor"

async def test_failed_modify_does_not_consume_slot(
self,
Expand Down Expand Up @@ -596,7 +641,7 @@ async def _failing_then_ok(call):
# First modify failed so slot 1 should be reused for the second user
assert len(modify_calls) == 1
assert modify_calls[0]["id"] == "201"
assert modify_calls[0]["name"] == "[LCM:1] Visitor B"
assert modify_calls[0]["name"] == "lcm:1:Visitor B"

async def test_no_managed_slots_is_noop(
self,
Expand Down Expand Up @@ -730,7 +775,7 @@ async def test_hard_refresh_tags_then_reads(
}
tagged_response = {
LOCK_ENTITY_ID: {
"users": [make_user("200", "[LCM:1] Visitor", "9999")],
"users": [make_user("200", "lcm:1:Visitor", "9999")],
},
}
list_handler = AsyncMock(side_effect=[untagged_response, tagged_response])
Expand Down Expand Up @@ -777,7 +822,7 @@ async def test_set_and_get(
# After setting, the mock now returns the tagged user with the PIN
after_response = {
LOCK_ENTITY_ID: {
"users": [make_user("100", "[LCM:1] base_test", "7777")],
"users": [make_user("100", "lcm:1:base_test", "7777")],
},
}
register_mock_service(
Expand All @@ -797,7 +842,7 @@ async def test_clear(
"""async_delete_credential + async_get_usercodes base projection shows empty."""
with_user = {
LOCK_ENTITY_ID: {
"users": [make_user("100", "[LCM:1] Guest", "1234")],
"users": [make_user("100", "lcm:1:Guest", "1234")],
},
}
register_mock_service(
Expand Down
12 changes: 6 additions & 6 deletions tests/providers/schlage/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async def test_set_credential(
assert result is True
assert schlage_mock_services["add_code"].call_count >= 1
add_call = schlage_mock_services["add_code"].call_args[0][0]
assert add_call.data["name"] == "[LCM:1] Test User"
assert add_call.data["name"] == "lcm:1:Test User"
assert add_call.data["code"] == "9999"

async def test_delete_credential(
Expand All @@ -84,7 +84,7 @@ async def test_delete_credential(
schlage_mock_services["get_codes"] = AsyncMock(
return_value={
entity_id: {
"code1": {"name": "[LCM:1] Guest", "code": "****"},
"code1": {"name": "lcm:1:Guest", "code": "****"},
},
}
)
Expand Down Expand Up @@ -113,7 +113,7 @@ async def test_base_orchestration_set_credential(
# internal returns None (fires coordinator refresh) — check service call
assert schlage_mock_services["add_code"].call_count >= 1
add_call = schlage_mock_services["add_code"].call_args[0][0]
assert add_call.data["name"] == "[LCM:1] Test User"
assert add_call.data["name"] == "lcm:1:Test User"
assert add_call.data["code"] == "9999"

async def test_coordinator_reflects_set_credential(
Expand All @@ -135,7 +135,7 @@ async def test_coordinator_reflects_set_credential(
schlage_mock_services["get_codes"] = AsyncMock(
return_value={
entity_id: {
"code1": {"name": "[LCM:1] Test User", "code": "****"},
"code1": {"name": "lcm:1:Test User", "code": "****"},
},
}
)
Expand Down Expand Up @@ -193,7 +193,7 @@ async def test_get_users_returns_codes(
schlage_mock_services["get_codes"] = AsyncMock(
return_value={
entity_id: {
"code1": {"name": "[LCM:1] Guest", "code": "****"},
"code1": {"name": "lcm:1:Guest", "code": "****"},
},
}
)
Expand Down Expand Up @@ -225,7 +225,7 @@ async def test_get_usercodes_projection(
schlage_mock_services["get_codes"] = AsyncMock(
return_value={
entity_id: {
"code1": {"name": "[LCM:1] Guest", "code": "****"},
"code1": {"name": "lcm:1:Guest", "code": "****"},
},
}
)
Expand Down
Loading
Loading