Changeset View
Changeset View
Standalone View
Standalone View
src/tests/client/device-list-tracker-test.cpp
| Show All 9 Lines | |||||
| #include <client-model.hpp> | #include <client-model.hpp> | ||||
| #include <actions/encryption.hpp> | #include <actions/encryption.hpp> | ||||
| #include <factory.hpp> | #include <factory.hpp> | ||||
| #include <debug.hpp> | #include <debug.hpp> | ||||
| using namespace Kazv; | using namespace Kazv; | ||||
| using namespace Kazv::Factory; | using namespace Kazv::Factory; | ||||
| TEST_CASE("DeviceListTracker marks devices as deleted but keeps them", "[client][device-list-tracker]") | |||||
| { | |||||
| DeviceListTracker tracker; | |||||
| auto info1 = makeDeviceKeyInfo(withDeviceId("device1") | withDeviceTrustLevel(Verified)); | |||||
| auto info2 = makeDeviceKeyInfo(withDeviceId("device2") | withDeviceTrustLevel(Verified)); | |||||
| auto userId = "@user:example.com"; | |||||
| tracker.addVerifiedDeviceKeyInfo(userId, "device1", info1); | |||||
| tracker.addVerifiedDeviceKeyInfo(userId, "device2", info2); | |||||
| REQUIRE(tracker.devicesFor(userId).size() == 2); | |||||
| // Mark device2 as deleted | |||||
| tracker.markDeviceAsDeleted(userId, "device2"); | |||||
| // The device is still in the tracker (for verifying past events) | |||||
| REQUIRE(tracker.devicesFor(userId).size() == 2); | |||||
| REQUIRE(tracker.get(userId, "device2").has_value()); | |||||
| // But it is marked as deleted | |||||
| REQUIRE(tracker.get(userId, "device2").value().deleted); | |||||
| REQUIRE(!tracker.get(userId, "device1").value().deleted); | |||||
| // findByCurve25519Key still finds the deleted device | |||||
| REQUIRE(tracker.findByCurve25519Key(userId, info2.curve25519Key).has_value()); | |||||
| REQUIRE(tracker.findByCurve25519Key(userId, info2.curve25519Key).value().deleted); | |||||
| } | |||||
| TEST_CASE("devicesToSendKeys excludes deleted devices", "[client][device-list-tracker]") | |||||
| { | |||||
| auto info1 = makeDeviceKeyInfo(withDeviceId("device1") | withDeviceTrustLevel(Verified)); | |||||
| auto info2 = makeDeviceKeyInfo(withDeviceId("device2") | withDeviceTrustLevel(Verified)); | |||||
| auto userId = "@user:example.com"; | |||||
| auto client = makeClient( | |||||
| withDevice(userId, info1) | |||||
| | withDevice(userId, info2) | |||||
| ); | |||||
| client.trustLevelNeededToSendKeys = Verified; | |||||
| REQUIRE(client.deviceLists.devicesFor(userId).size() == 2); | |||||
| REQUIRE(client.devicesToSendKeys(userId).size() == 2); | |||||
| // Mark device2 as deleted | |||||
| client.deviceLists.markDeviceAsDeleted(userId, "device2"); | |||||
| // devicesToSendKeys should now exclude the deleted device | |||||
| auto devices = client.devicesToSendKeys(userId); | |||||
| REQUIRE(devices.size() == 1); | |||||
| REQUIRE(devices[0] == "device1"); | |||||
| } | |||||
| static json makeSignedDeviceInfo(const ClientModel &client) | |||||
| { | |||||
| auto [next, _] = updateClient(client, UploadIdentityKeysAction{}); | |||||
| return json::parse(std::get<Bytes>(next.nextJobs[0].requestBody()))["device_keys"]; | |||||
| } | |||||
| TEST_CASE("processResponse(QueryKeysResponse) marks deleted devices on full query", "[client][device-list-tracker]") | |||||
| { | |||||
| // Set up two device identity cryptos for the same user | |||||
| auto device1Crypto = makeCrypto(); | |||||
| auto device1Client = makeClient(withCrypto(device1Crypto)); | |||||
| device1Client.userId = "@alice:example.com"; | |||||
| device1Client.deviceId = "device1"; | |||||
| auto device2Crypto = makeCrypto(); | |||||
| auto device2Client = makeClient(withCrypto(device2Crypto)); | |||||
| device2Client.userId = "@alice:example.com"; | |||||
| device2Client.deviceId = "device2"; | |||||
| // Build a full key query response that contains BOTH devices | |||||
| auto bothDevicesRespJson = json{ | |||||
| {"device_keys", {{"@alice:example.com", { | |||||
| {"device1", makeSignedDeviceInfo(device1Client)}, | |||||
| {"device2", makeSignedDeviceInfo(device2Client)}, | |||||
| }}}}, | |||||
| }; | |||||
| // Our client tracking Alice's devices | |||||
| auto client = makeClient(withCrypto(makeCrypto())); | |||||
| client.userId = "@bob:example.com"; | |||||
| client.deviceId = "bobdevice"; | |||||
| // Process the response with both devices | |||||
| std::tie(client, std::ignore) = processResponse(client, QueryKeysResponse( | |||||
| makeResponse("QueryKeys", | |||||
| withResponseJsonBody(bothDevicesRespJson) | |||||
| | withResponseDataKV("deviceKeys", | |||||
| json::object({{"@alice:example.com", json::array()}}))))); | |||||
| REQUIRE(client.deviceLists.devicesFor("@alice:example.com").size() == 2); | |||||
| REQUIRE(client.deviceLists.get("@alice:example.com", "device1").has_value()); | |||||
| REQUIRE(client.deviceLists.get("@alice:example.com", "device2").has_value()); | |||||
| REQUIRE(!client.deviceLists.get("@alice:example.com", "device2").value().deleted); | |||||
| // Now Alice deletes device2. We get a new full key query response | |||||
| // that only contains device1. | |||||
| auto onlyDevice1RespJson = json{ | |||||
| {"device_keys", {{"@alice:example.com", { | |||||
| {"device1", makeSignedDeviceInfo(device1Client)}, | |||||
| }}}}, | |||||
| }; | |||||
| std::tie(client, std::ignore) = processResponse(client, QueryKeysResponse( | |||||
| makeResponse("QueryKeys2", | |||||
| withResponseJsonBody(onlyDevice1RespJson) | |||||
| | withResponseDataKV("deviceKeys", | |||||
| json::object({{"@alice:example.com", json::array()}}))))); | |||||
| // device2 is kept in the tracker (for verifying past events) but marked deleted | |||||
| REQUIRE(client.deviceLists.devicesFor("@alice:example.com").size() == 2); | |||||
| REQUIRE(client.deviceLists.get("@alice:example.com", "device2").has_value()); | |||||
| REQUIRE(client.deviceLists.get("@alice:example.com", "device2").value().deleted); | |||||
| // device1 is not deleted | |||||
| REQUIRE(!client.deviceLists.get("@alice:example.com", "device1").value().deleted); | |||||
| // devicesToSendKeys excludes the deleted device | |||||
| client.trustLevelNeededToSendKeys = Unseen; | |||||
| auto devices = client.devicesToSendKeys("@alice:example.com"); | |||||
| REQUIRE(devices.size() == 1); | |||||
| REQUIRE(devices[0] == "device1"); | |||||
| } | |||||
| static ClientModel makeDeviceClient(std::string userId, std::string deviceId) | |||||
| { | |||||
| auto client = makeClient(withCrypto(makeCrypto())); | |||||
| client.userId = userId; | |||||
| client.deviceId = deviceId; | |||||
| return client; | |||||
| } | |||||
| TEST_CASE("mixed full and specific queries: full marks deleted, specific preserves unqueried", "[client][device-list-tracker]") | |||||
| { | |||||
| // Create crypto identities for all devices of two users | |||||
| auto aliceDev1 = makeDeviceClient("@alice:example.com", "device1"); | |||||
| auto aliceDev2 = makeDeviceClient("@alice:example.com", "device2"); | |||||
| auto aliceDev3 = makeDeviceClient("@alice:example.com", "device3"); | |||||
| auto bobDev1 = makeDeviceClient("@bob:example.com", "device1"); | |||||
| auto bobDev2 = makeDeviceClient("@bob:example.com", "device2"); | |||||
| // Our client tracking both Alice and Bob | |||||
| auto client = makeClient(withCrypto(makeCrypto())); | |||||
| client.userId = "@carol:example.com"; | |||||
| client.deviceId = "caroldevice"; | |||||
| // Process an initial full query that discovers all devices for both users | |||||
| auto initialRespJson = json{ | |||||
| {"device_keys", { | |||||
| {"@alice:example.com", { | |||||
| {"device1", makeSignedDeviceInfo(aliceDev1)}, | |||||
| {"device2", makeSignedDeviceInfo(aliceDev2)}, | |||||
| {"device3", makeSignedDeviceInfo(aliceDev3)}, | |||||
| }}, | |||||
| {"@bob:example.com", { | |||||
| {"device1", makeSignedDeviceInfo(bobDev1)}, | |||||
| {"device2", makeSignedDeviceInfo(bobDev2)}, | |||||
| }}, | |||||
| }}, | |||||
| }; | |||||
| std::tie(client, std::ignore) = processResponse(client, QueryKeysResponse( | |||||
| makeResponse("QueryKeys", | |||||
| withResponseJsonBody(initialRespJson) | |||||
| | withResponseDataKV("deviceKeys", | |||||
| json::object({ | |||||
| {"@alice:example.com", json::array()}, | |||||
| {"@bob:example.com", json::array()}, | |||||
| }))))); | |||||
| // All devices are now tracked | |||||
| REQUIRE(client.deviceLists.devicesFor("@alice:example.com").size() == 3); | |||||
| REQUIRE(client.deviceLists.devicesFor("@bob:example.com").size() == 2); | |||||
| // Now Alice deletes device3. We do a mixed query in a single request: | |||||
| // - Full query for Alice (empty list = all devices) | |||||
| // -> response has only device1, device2 (device3 was deleted) | |||||
| // - Specific query for Bob with ["device1"] only | |||||
| // -> response has only device1 (device2 was not queried) | |||||
| auto mixedRespJson = json{ | |||||
| {"device_keys", { | |||||
| {"@alice:example.com", { | |||||
| {"device1", makeSignedDeviceInfo(aliceDev1)}, | |||||
| {"device2", makeSignedDeviceInfo(aliceDev2)}, | |||||
| // device3 deleted, not in response | |||||
| }}, | |||||
| {"@bob:example.com", { | |||||
| {"device1", makeSignedDeviceInfo(bobDev1)}, | |||||
| // device2 not queried, not in response | |||||
| }}, | |||||
| }}, | |||||
| }; | |||||
| std::tie(client, std::ignore) = processResponse(client, QueryKeysResponse( | |||||
| makeResponse("QueryKeys2", | |||||
| withResponseJsonBody(mixedRespJson) | |||||
| | withResponseDataKV("deviceKeys", | |||||
| json::object({ | |||||
| {"@alice:example.com", json::array()}, // full query: all devices | |||||
| {"@bob:example.com", json::array({"device1"})}, // specific query: device1 only | |||||
| }))))); | |||||
| // For Alice (full query): device3 was deleted and NOT in the response. | |||||
| // device3 is kept in the tracker (for verifying past events) but marked | |||||
| // as deleted. | |||||
| REQUIRE(client.deviceLists.devicesFor("@alice:example.com").size() == 3); | |||||
| REQUIRE(client.deviceLists.get("@alice:example.com", "device3").has_value()); | |||||
| REQUIRE(client.deviceLists.get("@alice:example.com", "device3").value().deleted); | |||||
| REQUIRE(!client.deviceLists.get("@alice:example.com", "device1").value().deleted); | |||||
| REQUIRE(!client.deviceLists.get("@alice:example.com", "device2").value().deleted); | |||||
| // For Bob (specific query for device1 only): device2 was NOT in the | |||||
| // request and NOT in the response. Since we did not query for all | |||||
| // of Bob's devices, absence from the response does not mean deletion. | |||||
| // device2 must be preserved and NOT marked as deleted. | |||||
| REQUIRE(client.deviceLists.devicesFor("@bob:example.com").size() == 2); | |||||
| REQUIRE(client.deviceLists.get("@bob:example.com", "device2").has_value()); | |||||
| REQUIRE(!client.deviceLists.get("@bob:example.com", "device2").value().deleted); | |||||
| REQUIRE(client.deviceLists.get("@bob:example.com", "device1").has_value()); | |||||
| REQUIRE(!client.deviceLists.get("@bob:example.com", "device1").value().deleted); | |||||
| // devicesToSendKeys excludes Alice's deleted device3 | |||||
| client.trustLevelNeededToSendKeys = Unseen; | |||||
| auto aliceDevices = client.devicesToSendKeys("@alice:example.com"); | |||||
| REQUIRE(aliceDevices.size() == 2); | |||||
| // Bob's devicesToSendKeys correctly includes both devices | |||||
| auto bobDevices = client.devicesToSendKeys("@bob:example.com"); | |||||
| REQUIRE(bobDevices.size() == 2); | |||||
| } | |||||
| TEST_CASE("DeviceListTracker::findByCurve25519Key") | TEST_CASE("DeviceListTracker::findByCurve25519Key") | ||||
| { | { | ||||
| auto info = makeDeviceKeyInfo(withDeviceId("device1")); | auto info = makeDeviceKeyInfo(withDeviceId("device1")); | ||||
| auto client = makeClient(withDevice("@example:example.com", info)); | auto client = makeClient(withDevice("@example:example.com", info)); | ||||
| REQUIRE(client.deviceLists.findByCurve25519Key("@example:example.com", info.curve25519Key) == info); | REQUIRE(client.deviceLists.findByCurve25519Key("@example:example.com", info.curve25519Key) == info); | ||||
| REQUIRE(client.deviceLists.findByCurve25519Key("@someone:example.com", info.curve25519Key) == std::nullopt); | REQUIRE(client.deviceLists.findByCurve25519Key("@someone:example.com", info.curve25519Key) == std::nullopt); | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 71 Lines • Show Last 20 Lines | |||||