@@ -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