@@ -235,3 +235,181 @@ def decrypt(self, packet, padding_obj):
235235 assert abs (loc3 .lat - 15.0 ) < 1e-6
236236 finally :
237237 await client .close ()
238+
239+ @pytest .mark .asyncio
240+ async def test_device_cached_location_property ():
241+ """Test Device.cached_location property access."""
242+ client = FmdClient ("https://fmd.example.com" )
243+ client .access_token = "token"
244+ class DummyKey :
245+ def decrypt (self , packet , padding_obj ):
246+ return b'\x00 ' * 32
247+ client .private_key = DummyKey ()
248+
249+ from cryptography .hazmat .primitives .ciphers .aead import AESGCM
250+ session_key = b'\x00 ' * 32
251+ aesgcm = AESGCM (session_key )
252+ iv = b'\x06 ' * 12
253+ plaintext = b'{"lat":20.0,"lon":30.0,"date":1600000000000,"bat":75}'
254+ ciphertext = aesgcm .encrypt (iv , plaintext , None )
255+ blob = b'\xAA ' * 384 + iv + ciphertext
256+ blob_b64 = base64 .b64encode (blob ).decode ('utf-8' ).rstrip ('=' )
257+
258+ client .access_token = "token"
259+ device = Device (client , "test-device" )
260+
261+ # Initially None
262+ assert device .cached_location is None
263+
264+ with aioresponses () as m :
265+ m .put ("https://fmd.example.com/api/v1/locationDataSize" , payload = {"Data" : "1" })
266+ m .put ("https://fmd.example.com/api/v1/location" , payload = {"Data" : blob_b64 })
267+ try :
268+ await device .refresh ()
269+ # Now should have cached location
270+ assert device .cached_location is not None
271+ assert abs (device .cached_location .lat - 20.0 ) < 1e-6
272+ finally :
273+ await client .close ()
274+
275+ @pytest .mark .asyncio
276+ async def test_device_refresh_without_force ():
277+ """Test Device.refresh with force=False doesn't re-fetch if cached."""
278+ client = FmdClient ("https://fmd.example.com" )
279+ client .access_token = "token"
280+ class DummyKey :
281+ def decrypt (self , packet , padding_obj ):
282+ return b'\x00 ' * 32
283+ client .private_key = DummyKey ()
284+
285+ from cryptography .hazmat .primitives .ciphers .aead import AESGCM
286+ session_key = b'\x00 ' * 32
287+ aesgcm = AESGCM (session_key )
288+ iv = b'\x07 ' * 12
289+ plaintext = b'{"lat":25.0,"lon":35.0,"date":1600000000000,"bat":85}'
290+ ciphertext = aesgcm .encrypt (iv , plaintext , None )
291+ blob = b'\xAA ' * 384 + iv + ciphertext
292+ blob_b64 = base64 .b64encode (blob ).decode ('utf-8' ).rstrip ('=' )
293+
294+ client .access_token = "token"
295+ device = Device (client , "test-device" )
296+
297+ with aioresponses () as m :
298+ # Only one set of mocks
299+ m .put ("https://fmd.example.com/api/v1/locationDataSize" , payload = {"Data" : "1" })
300+ m .put ("https://fmd.example.com/api/v1/location" , payload = {"Data" : blob_b64 })
301+ try :
302+ await device .refresh ()
303+ loc1 = device .cached_location
304+
305+ # Second refresh without force should not make HTTP calls (mocks would fail if it did)
306+ await device .refresh (force = False )
307+ loc2 = device .cached_location
308+
309+ assert loc1 is loc2 # Same cached object
310+ finally :
311+ await client .close ()
312+
313+ @pytest .mark .asyncio
314+ async def test_device_picture_commands ():
315+ """Test Device picture-related command shortcuts."""
316+ client = FmdClient ("https://fmd.example.com" )
317+ client .access_token = "token"
318+ class DummySigner :
319+ def sign (self , message_bytes , pad , algo ):
320+ return b"\xAB " * 64
321+ client .private_key = DummySigner ()
322+
323+ await client ._ensure_session ()
324+ device = Device (client , "test-device" )
325+
326+ with aioresponses () as m :
327+ # take_front_photo
328+ m .post ("https://fmd.example.com/api/v1/command" , status = 200 , body = "OK" )
329+ # take_rear_photo
330+ m .post ("https://fmd.example.com/api/v1/command" , status = 200 , body = "OK" )
331+
332+ try :
333+ result1 = await device .take_front_photo ()
334+ assert result1 is True
335+
336+ result2 = await device .take_rear_photo ()
337+ assert result2 is True
338+ finally :
339+ await client .close ()
340+
341+ @pytest .mark .asyncio
342+ async def test_device_lock_with_message ():
343+ """Test Device.lock with custom message."""
344+ client = FmdClient ("https://fmd.example.com" )
345+ client .access_token = "token"
346+ class DummySigner :
347+ def sign (self , message_bytes , pad , algo ):
348+ return b"\xAB " * 64
349+ client .private_key = DummySigner ()
350+
351+ await client ._ensure_session ()
352+ device = Device (client , "test-device" )
353+
354+ with aioresponses () as m :
355+ m .post ("https://fmd.example.com/api/v1/command" , status = 200 , body = "OK" )
356+
357+ try :
358+ result = await device .lock ("Please return this device" )
359+ assert result is True
360+ finally :
361+ await client .close ()
362+
363+ @pytest .mark .asyncio
364+ async def test_device_multiple_history_calls ():
365+ """Test Device.get_history can be called multiple times."""
366+ client = FmdClient ("https://fmd.example.com" )
367+ client .access_token = "token"
368+ class DummyKey :
369+ def decrypt (self , packet , padding_obj ):
370+ return b'\x00 ' * 32
371+ client .private_key = DummyKey ()
372+
373+ from cryptography .hazmat .primitives .ciphers .aead import AESGCM
374+ session_key = b'\x00 ' * 32
375+ aesgcm = AESGCM (session_key )
376+
377+ blobs = []
378+ for i in range (2 ):
379+ iv = bytes ([i + 10 ] * 12 )
380+ plaintext = json .dumps ({"lat" : float (30 + i ), "lon" : float (40 + i ), "date" : 1600000000000 + i * 1000 , "bat" : 80 }).encode ('utf-8' )
381+ ciphertext = aesgcm .encrypt (iv , plaintext , None )
382+ blob = b'\xAA ' * 384 + iv + ciphertext
383+ blobs .append (base64 .b64encode (blob ).decode ('utf-8' ).rstrip ('=' ))
384+
385+ client .access_token = "token"
386+ device = Device (client , "test-device" )
387+
388+ with aioresponses () as m :
389+ # First call to get_history
390+ m .put ("https://fmd.example.com/api/v1/locationDataSize" , payload = {"Data" : "2" })
391+ m .put ("https://fmd.example.com/api/v1/location" , payload = {"Data" : blobs [0 ]})
392+ m .put ("https://fmd.example.com/api/v1/location" , payload = {"Data" : blobs [1 ]})
393+
394+ # Second call to get_history
395+ m .put ("https://fmd.example.com/api/v1/locationDataSize" , payload = {"Data" : "2" })
396+ m .put ("https://fmd.example.com/api/v1/location" , payload = {"Data" : blobs [0 ]})
397+ m .put ("https://fmd.example.com/api/v1/location" , payload = {"Data" : blobs [1 ]})
398+
399+ try :
400+ # First iteration
401+ locs1 = []
402+ async for loc in device .get_history (limit = 2 ):
403+ locs1 .append (loc )
404+ assert len (locs1 ) == 2
405+
406+ # Second iteration (should work independently)
407+ locs2 = []
408+ async for loc in device .get_history (limit = 2 ):
409+ locs2 .append (loc )
410+ assert len (locs2 ) == 2
411+
412+ # Both should have same data (different Location objects)
413+ assert abs (locs1 [0 ].lat - locs2 [0 ].lat ) < 1e-6
414+ finally :
415+ await client .close ()
0 commit comments