|
| 1 | +import asyncio |
| 2 | +import json |
| 3 | +import base64 |
| 4 | +from datetime import datetime, timezone |
| 5 | + |
| 6 | +import pytest |
| 7 | +import aiohttp |
| 8 | +from aioresponses import aioresponses |
| 9 | + |
| 10 | +from fmd_api.client import FmdClient |
| 11 | +from fmd_api.helpers import _pad_base64 |
| 12 | + |
| 13 | +# NOTE: These tests validate behavior parity for the core HTTP flows using mocks. |
| 14 | +# They do not perform full Argon2/RSA cryptography verification, but they assert |
| 15 | +# that the client calls the expected endpoints and behaves like the original client. |
| 16 | + |
| 17 | +@pytest.mark.asyncio |
| 18 | +async def test_get_locations_and_decrypt(monkeypatch): |
| 19 | + # Create a fake client and stub methods that require heavy crypto with small helpers. |
| 20 | + client = FmdClient("https://fmd.example.com") |
| 21 | + # Provide a dummy private_key with a decrypt method for testing |
| 22 | + class DummyKey: |
| 23 | + def decrypt(self, packet, padding_obj): |
| 24 | + # Return a 32-byte AES session key for AESGCM, for tests we use 32 zero bytes |
| 25 | + return b"\x00" * 32 |
| 26 | + client.private_key = DummyKey() |
| 27 | + |
| 28 | + # Build a fake AES-GCM encrypted payload: we'll create plaintext b'{"lat":1.0,"lon":2.0,"date":1234,"bat":50}' |
| 29 | + plaintext = b'{"lat":1.0,"lon":2.0,"date":1600000000000,"bat":50}' |
| 30 | + # For the test, simulate AESGCM by encrypting with a known key using AESGCM class |
| 31 | + from cryptography.hazmat.primitives.ciphers.aead import AESGCM |
| 32 | + session_key = b"\x00" * 32 |
| 33 | + aesgcm = AESGCM(session_key) |
| 34 | + iv = b"\x01" * 12 |
| 35 | + ciphertext = aesgcm.encrypt(iv, plaintext, None) |
| 36 | + # Build blob: session_key_packet (RSA_KEY_SIZE_BYTES) + iv + ciphertext |
| 37 | + session_key_packet = b"\xAA" * 384 # dummy RSA packet; DummyKey.decrypt ignores it |
| 38 | + blob = session_key_packet + iv + ciphertext |
| 39 | + blob_b64 = base64.b64encode(blob).decode('utf-8').rstrip('=') |
| 40 | + |
| 41 | + # Mock the endpoints used by get_locations: |
| 42 | + with aioresponses() as m: |
| 43 | + m.put("https://fmd.example.com/api/v1/locationDataSize", payload={"Data": "1"}) |
| 44 | + m.put("https://fmd.example.com/api/v1/location", payload=blob_b64) |
| 45 | + client.access_token = "dummy-token" |
| 46 | + locations = await client.get_locations(num_to_get=1) |
| 47 | + assert len(locations) == 1 |
| 48 | + decrypted = client.decrypt_data_blob(locations[0]) |
| 49 | + assert b'"lat":1.0' in decrypted |
| 50 | + assert b'"lon":2.0' in decrypted |
| 51 | + |
| 52 | +@pytest.mark.asyncio |
| 53 | +async def test_send_command_reauth(monkeypatch): |
| 54 | + client = FmdClient("https://fmd.example.com") |
| 55 | + # create a dummy private key with sign() |
| 56 | + class DummySigner: |
| 57 | + def sign(self, message_bytes, pad, algo): |
| 58 | + return b"\xAB" * 64 |
| 59 | + client.private_key = DummySigner() |
| 60 | + client._fmd_id = "id" |
| 61 | + client._password = "pw" |
| 62 | + client.access_token = "old-token" |
| 63 | + |
| 64 | + with aioresponses() as m: |
| 65 | + # First POST returns 401 -> client should re-authenticate |
| 66 | + m.post("https://fmd.example.com/api/v1/command", status=401) |
| 67 | + # When authenticate is called during reauth, stub the internal calls: |
| 68 | + async def fake_authenticate(fmd_id, password, session_duration): |
| 69 | + client.access_token = "new-token" |
| 70 | + monkeypatch.setattr(client, "authenticate", fake_authenticate) |
| 71 | + # Second attempt should now succeed |
| 72 | + m.post("https://fmd.example.com/api/v1/command", status=200, body="OK") |
| 73 | + res = await client.send_command("ring") |
| 74 | + assert res is True |
| 75 | + |
| 76 | +@pytest.mark.asyncio |
| 77 | +async def test_export_data_zip_stream(monkeypatch, tmp_path): |
| 78 | + client = FmdClient("https://fmd.example.com") |
| 79 | + client.access_token = "token" |
| 80 | + small_zip = b'PK\x03\x04' + b'\x00' * 100 |
| 81 | + with aioresponses() as m: |
| 82 | + m.post("https://fmd.example.com/api/v1/exportData", body=small_zip, status=200) |
| 83 | + out_file = tmp_path / "export.zip" |
| 84 | + await client.export_data_zip(str(out_file)) |
| 85 | + assert out_file.exists() |
| 86 | + content = out_file.read_bytes() |
| 87 | + assert content.startswith(b'PK\x03\x04') |
0 commit comments