diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java index 2943d8d0e..c88165dc2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -915,7 +915,7 @@ public class AccountsManager { private void redisSet(Account account) { try (Timer.Context ignored = redisSetTimer.time()) { - final String accountJson = ACCOUNT_REDIS_JSON_WRITER.writeValueAsString(account); + final String accountJson = writeRedisAccountJson(account); cacheCluster.useCluster(connection -> { final RedisAdvancedClusterCommands commands = connection.sync(); @@ -936,7 +936,7 @@ public class AccountsManager { final String accountJson; try { - accountJson = ACCOUNT_REDIS_JSON_WRITER.writeValueAsString(account); + accountJson = writeRedisAccountJson(account); } catch (final JsonProcessingException e) { throw new UncheckedIOException(e); } @@ -1047,7 +1047,8 @@ public class AccountsManager { .toCompletableFuture(); } - private static Optional parseAccountJson(@Nullable final String accountJson, final UUID uuid) { + @VisibleForTesting + static Optional parseAccountJson(@Nullable final String accountJson, final UUID uuid) { try { if (StringUtils.isNotBlank(accountJson)) { Account account = SystemMapper.jsonMapper().readValue(accountJson, Account.class); @@ -1067,6 +1068,11 @@ public class AccountsManager { } } + @VisibleForTesting + static String writeRedisAccountJson(final Account account) throws JsonProcessingException { + return ACCOUNT_REDIS_JSON_WRITER.writeValueAsString(account); + } + private void redisDelete(final Account account) { try (final Timer.Context ignored = redisDeleteTimer.time()) { cacheCluster.useCluster(connection -> { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java index 8810ca7f3..4e10a4977 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java @@ -31,6 +31,8 @@ import static org.mockito.Mockito.when; import io.lettuce.core.RedisException; import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.time.Clock; import java.time.Duration; import java.util.ArrayList; @@ -39,6 +41,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -1328,6 +1331,46 @@ class AccountsManagerTest { assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setUsernameHash(USERNAME_HASH_1))); } + @Test + void testJsonRoundTripSerialization() throws Exception { + String originalJson; + try (InputStream inputStream = getClass().getResourceAsStream( + "AccountsManagerTest-testJsonRoundTripSerialization.json")) { + Objects.requireNonNull(inputStream); + originalJson = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + + final Account originalAccount = AccountsManager.parseAccountJson(originalJson, + UUID.fromString("111111-1111-1111-1111-111111111111")).orElseThrow(); + + final String serialized = AccountsManager.writeRedisAccountJson(originalAccount); + final Account parsedAccount = AccountsManager.parseAccountJson(serialized, originalAccount.getUuid()).orElseThrow(); + + assertEquals(originalAccount.getUuid(), parsedAccount.getUuid()); + assertEquals(originalAccount.getPhoneNumberIdentifier(), parsedAccount.getPhoneNumberIdentifier()); + assertEquals(originalAccount.getIdentityKey(IdentityType.ACI), parsedAccount.getIdentityKey(IdentityType.ACI)); + assertEquals(originalAccount.getIdentityKey(IdentityType.PNI), parsedAccount.getIdentityKey(IdentityType.PNI)); + assertEquals(originalAccount.getNumber(), parsedAccount.getNumber()); + assertArrayEquals(originalAccount.getUnidentifiedAccessKey().orElseThrow(), + parsedAccount.getUnidentifiedAccessKey().orElseThrow()); + assertEquals(originalAccount.isDiscoverableByPhoneNumber(), parsedAccount.isDiscoverableByPhoneNumber()); + assertEquals(originalAccount.isUnrestrictedUnidentifiedAccess(), parsedAccount.isUnrestrictedUnidentifiedAccess()); + + assertEquals(originalAccount.getDevices().size(), parsedAccount.getDevices().size()); + + final Device originalDevice = originalAccount.getMasterDevice().orElseThrow(); + final Device parsedDevice = parsedAccount.getMasterDevice().orElseThrow(); + + assertEquals(originalDevice.getId(), parsedDevice.getId()); + assertEquals(originalDevice.getSignedPreKey(IdentityType.ACI), parsedDevice.getSignedPreKey(IdentityType.ACI)); + assertEquals(originalDevice.getSignedPreKey(IdentityType.PNI), parsedDevice.getSignedPreKey(IdentityType.PNI)); + assertEquals(originalDevice.getRegistrationId(), parsedDevice.getRegistrationId()); + assertEquals(originalDevice.getPhoneNumberIdentityRegistrationId(), + parsedDevice.getPhoneNumberIdentityRegistrationId()); + assertEquals(originalDevice.getCapabilities(), parsedDevice.getCapabilities()); + assertEquals(originalDevice.getFetchesMessages(), parsedDevice.getFetchesMessages()); + } + private void setReservationHash(final Account account, final byte[] reservedUsernameHash) { account.setReservedUsernameHash(reservedUsernameHash); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java index 2c21df2cd..64458a4ff 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java @@ -183,13 +183,13 @@ class AccountsTest { accounts.create(account); final UUID linkHandle = UUID.randomUUID(); - final byte[] encruptedUsername = RandomUtils.nextBytes(32); - accountsManager.update(account, a -> a.setUsernameLinkDetails(linkHandle, encruptedUsername)); + final byte[] encryptedUsername = RandomUtils.nextBytes(32); + accountsManager.update(account, a -> a.setUsernameLinkDetails(linkHandle, encryptedUsername)); final Optional maybeAccount = accountsManager.getByUsernameLinkHandle(linkHandle); assertTrue(maybeAccount.isPresent()); assertTrue(maybeAccount.get().getEncryptedUsername().isPresent()); - assertArrayEquals(encruptedUsername, maybeAccount.get().getEncryptedUsername().get()); + assertArrayEquals(encryptedUsername, maybeAccount.get().getEncryptedUsername().get()); // making some unrelated change and updating account to check that username link data is still there final Optional accountToChange = accountsManager.getByAccountIdentifier(account.getUuid()); @@ -198,7 +198,7 @@ class AccountsTest { final Optional accountAfterChange = accountsManager.getByUsernameLinkHandle(linkHandle); assertTrue(accountAfterChange.isPresent()); assertTrue(accountAfterChange.get().getEncryptedUsername().isPresent()); - assertArrayEquals(encruptedUsername, accountAfterChange.get().getEncryptedUsername().get()); + assertArrayEquals(encryptedUsername, accountAfterChange.get().getEncryptedUsername().get()); // now deleting the link final Optional accountToDeleteLink = accountsManager.getByAccountIdentifier(account.getUuid()); diff --git a/service/src/test/resources/org/whispersystems/textsecuregcm/storage/AccountsManagerTest-testJsonRoundTripSerialization.json b/service/src/test/resources/org/whispersystems/textsecuregcm/storage/AccountsManagerTest-testJsonRoundTripSerialization.json new file mode 100644 index 000000000..3163b9686 --- /dev/null +++ b/service/src/test/resources/org/whispersystems/textsecuregcm/storage/AccountsManagerTest-testJsonRoundTripSerialization.json @@ -0,0 +1,48 @@ +{ + "number": "+14152222222", + "usernameHash": null, + "reservedUsernameHash": null, + "usernameLinkHandle": null, + "devices": [ + { + "id": 1, + "name": null, + "authToken": null, + "salt": null, + "gcmId": null, + "apnId": null, + "voipApnId": null, + "pushTimestamp": 0, + "uninstalledFeedback": 0, + "fetchesMessages": true, + "registrationId": 1, + "signedPreKey": { + "keyId": 1, + "publicKey": "BerKjYSh1PdniL5bhI9kwbH/Et3mx/8CypR1TYo/+d5o", + "signature": "iK2yJkl0l6qe58Fy1dVo31X5sp6EiXSS5FZfa3W//E+Abylfa6ZRmM97CzTdXNu2DjgxZYF43G6HfJ49+99hgg" + }, + "lastSeen" : 1692748800000, + "created" : 1692718240137, + "userAgent": null, + "capabilities": null, + "pniRegistrationId": 2, + "pniSignedPreKey": { + "keyId": 2, + "publicKey": "BXcLL1VLft3tUnr/5UIW5Q0Hsr8/Az0CGJ+EuFqiXCYc", + "signature": "YoKqyeOCHC0E9mqMoc1UPeyuLqGc8nvY+3D3YX5HC1bhxS48ZLYo40xql51A2CpIBqVmA+2gV3PXCV1Yhq4UAQ" + } + } + ], + "identityKey": "BaMV4k/+jSn7jmHnRAPvfc7XBZOcayrhOmHFbGJwMyFS", + "badges": [], + "registrationLock": null, + "registrationLockSalt": null, + "version": 0, + "pni": "22222222-2222-2222-2222-222222222222", + "eu": null, + "pniIdentityKey": "Bc0Myhpf2D+iCgUfIs+UStgffR/VGQRfP9mwFHI4U2x4", + "cpv": null, + "uak": "p5uWNi83Muqsd16PLi0/tQ==", + "uua": true, + "inCds": true +}