Skip to content

Commit bf0ce65

Browse files
committed
50/50 passing unit tests. 28 client/22 device.
1 parent 6a0bda6 commit bf0ce65

2 files changed

Lines changed: 471 additions & 0 deletions

File tree

tests/unit/test_client.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,209 @@ async def test_authenticate_error_handling():
472472
await client.authenticate("bad_id", "bad_password", session_duration=3600)
473473
finally:
474474
await client.close()
475+
476+
@pytest.mark.asyncio
477+
async def test_get_locations_with_skip_empty_false():
478+
"""Test get_locations with skip_empty=False fetches all indices."""
479+
client = FmdClient("https://fmd.example.com")
480+
client.access_token = "token"
481+
482+
class DummyKey:
483+
def decrypt(self, packet, padding_obj):
484+
return b'\x00' * 32
485+
client.private_key = DummyKey()
486+
487+
await client._ensure_session()
488+
489+
with aioresponses() as m:
490+
m.put("https://fmd.example.com/api/v1/locationDataSize", payload={"Data": "2"})
491+
# Both empty and valid blobs
492+
m.put("https://fmd.example.com/api/v1/location", payload={"Data": ""})
493+
m.put("https://fmd.example.com/api/v1/location", payload={"Data": ""})
494+
495+
try:
496+
# With skip_empty=False, should return empty blobs too
497+
locs = await client.get_locations(num_to_get=2, skip_empty=False)
498+
# Both are empty strings, so should get empty list since empty strings are filtered
499+
assert len(locs) == 0
500+
finally:
501+
await client.close()
502+
503+
@pytest.mark.asyncio
504+
async def test_send_command_failure():
505+
"""Test send_command when server returns non-200 status."""
506+
client = FmdClient("https://fmd.example.com")
507+
client.access_token = "token"
508+
509+
class DummySigner:
510+
def sign(self, message_bytes, pad, algo):
511+
return b"\xAB" * 64
512+
client.private_key = DummySigner()
513+
514+
await client._ensure_session()
515+
516+
with aioresponses() as m:
517+
m.post("https://fmd.example.com/api/v1/command", status=500, body="Server Error")
518+
519+
try:
520+
from fmd_api.exceptions import FmdApiException
521+
with pytest.raises(FmdApiException, match="Failed to send command"):
522+
await client.send_command("ring")
523+
finally:
524+
await client.close()
525+
526+
@pytest.mark.asyncio
527+
async def test_decrypt_blob_invalid_format():
528+
"""Test decrypt_data_blob with malformed blob."""
529+
client = FmdClient("https://fmd.example.com")
530+
531+
class DummyKey:
532+
def decrypt(self, packet, padding_obj):
533+
return b'\x00' * 32
534+
client.private_key = DummyKey()
535+
536+
# Blob too short to contain IV and ciphertext
537+
short_blob = base64.b64encode(b'x' * 10).decode('utf-8')
538+
539+
from fmd_api.exceptions import FmdApiException
540+
with pytest.raises(FmdApiException, match="Blob too small"):
541+
client.decrypt_data_blob(short_blob)
542+
543+
@pytest.mark.asyncio
544+
async def test_export_data_404():
545+
"""Test export_data_zip when endpoint doesn't exist."""
546+
client = FmdClient("https://fmd.example.com")
547+
client.access_token = "token"
548+
549+
await client._ensure_session()
550+
551+
with aioresponses() as m:
552+
m.post("https://fmd.example.com/api/v1/exportData", status=404)
553+
554+
try:
555+
from fmd_api.exceptions import FmdApiException
556+
import tempfile
557+
with tempfile.NamedTemporaryFile(delete=False) as tmp:
558+
with pytest.raises(FmdApiException, match="exportData endpoint not found"):
559+
await client.export_data_zip(tmp.name)
560+
finally:
561+
await client.close()
562+
563+
@pytest.mark.asyncio
564+
async def test_get_pictures_empty_response():
565+
"""Test get_pictures when server returns empty list."""
566+
client = FmdClient("https://fmd.example.com")
567+
client.access_token = "token"
568+
569+
await client._ensure_session()
570+
571+
with aioresponses() as m:
572+
m.put("https://fmd.example.com/api/v1/pictures", payload={"Data": []})
573+
574+
try:
575+
pictures = await client.get_pictures()
576+
assert pictures == []
577+
finally:
578+
await client.close()
579+
580+
@pytest.mark.asyncio
581+
async def test_request_location_unknown_provider():
582+
"""Test request_location with unknown provider (falls back to 'locate')."""
583+
client = FmdClient("https://fmd.example.com")
584+
client.access_token = "token"
585+
class DummySigner:
586+
def sign(self, message_bytes, pad, algo):
587+
return b"\xAB" * 64
588+
client.private_key = DummySigner()
589+
590+
await client._ensure_session()
591+
592+
with aioresponses() as m:
593+
m.post("https://fmd.example.com/api/v1/command", status=200, body="OK")
594+
595+
try:
596+
# Unknown provider falls back to generic "locate" command
597+
result = await client.request_location(provider="unknown")
598+
assert result is True
599+
finally:
600+
await client.close()
601+
602+
@pytest.mark.asyncio
603+
async def test_close_without_session():
604+
"""Test close when no session exists."""
605+
client = FmdClient("https://fmd.example.com")
606+
# Should not raise error
607+
await client.close()
608+
assert client._session is None
609+
610+
@pytest.mark.asyncio
611+
async def test_multiple_close_calls():
612+
"""Test calling close multiple times."""
613+
client = FmdClient("https://fmd.example.com")
614+
await client._ensure_session()
615+
616+
# First close
617+
await client.close()
618+
assert client._session is None
619+
620+
# Second close should not raise error
621+
await client.close()
622+
assert client._session is None
623+
624+
@pytest.mark.asyncio
625+
async def test_get_locations_max_attempts():
626+
"""Test get_locations respects max_attempts parameter."""
627+
client = FmdClient("https://fmd.example.com")
628+
client.access_token = "token"
629+
630+
class DummyKey:
631+
def decrypt(self, packet, padding_obj):
632+
return b'\x00' * 32
633+
client.private_key = DummyKey()
634+
635+
await client._ensure_session()
636+
637+
with aioresponses() as m:
638+
m.put("https://fmd.example.com/api/v1/locationDataSize", payload={"Data": "100"})
639+
# Only set up 3 mocks even though max_attempts could be higher
640+
for i in range(3):
641+
m.put("https://fmd.example.com/api/v1/location", payload={"Data": ""})
642+
643+
try:
644+
# Request 1 location from 100 available, with max_attempts=3
645+
locs = await client.get_locations(num_to_get=1, skip_empty=True, max_attempts=3)
646+
# All 3 are empty, so should get empty list
647+
assert len(locs) == 0
648+
finally:
649+
await client.close()
650+
651+
@pytest.mark.asyncio
652+
async def test_set_ringer_mode_edge_cases():
653+
"""Test set_ringer_mode with all valid modes."""
654+
client = FmdClient("https://fmd.example.com")
655+
client.access_token = "token"
656+
657+
class DummySigner:
658+
def sign(self, message_bytes, pad, algo):
659+
return b"\xAB" * 64
660+
client.private_key = DummySigner()
661+
662+
await client._ensure_session()
663+
664+
with aioresponses() as m:
665+
# Mock for each valid mode
666+
m.post("https://fmd.example.com/api/v1/command", status=200, body="OK")
667+
m.post("https://fmd.example.com/api/v1/command", status=200, body="OK")
668+
m.post("https://fmd.example.com/api/v1/command", status=200, body="OK")
669+
670+
try:
671+
result1 = await client.set_ringer_mode("normal")
672+
assert result1 is True
673+
674+
result2 = await client.set_ringer_mode("vibrate")
675+
assert result2 is True
676+
677+
result3 = await client.set_ringer_mode("silent")
678+
assert result3 is True
679+
finally:
680+
await client.close()

0 commit comments

Comments
 (0)