mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 14:28:05 +01:00
Stored hashed username
This commit is contained in:
@@ -6,12 +6,12 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.anyLong;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
@@ -24,7 +24,6 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
@@ -37,6 +36,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -79,20 +79,20 @@ import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameResponse;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitByIpFilter;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberResponse;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
@@ -108,7 +108,7 @@ import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
@@ -119,7 +119,6 @@ import org.whispersystems.textsecuregcm.util.TestClock;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class AccountControllerTest {
|
||||
|
||||
private static final String SENDER = "+14152222222";
|
||||
private static final String SENDER_OLD = "+14151111111";
|
||||
private static final String SENDER_PIN = "+14153333333";
|
||||
@@ -131,10 +130,18 @@ class AccountControllerTest {
|
||||
private static final String SENDER_TRANSFER = "+14151111112";
|
||||
private static final String RESTRICTED_COUNTRY = "800";
|
||||
private static final String RESTRICTED_NUMBER = "+" + RESTRICTED_COUNTRY + "11111111";
|
||||
private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
|
||||
private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
|
||||
|
||||
private static final String INVALID_BASE_64_URL_USERNAME_HASH = "fA+VkNbvB6dVfx/6NpaRSK6mvhhAUBgDNWFaD7+7gvs=";
|
||||
private static final String TOO_SHORT_BASE_64_URL_USERNAME_HASH = "P2oMuxx0xgGxSpTO0ACq3IztEOBDaV9t9YFu4bAGpQ";
|
||||
private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1);
|
||||
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
|
||||
private static final byte[] INVALID_USERNAME_HASH = Base64.getDecoder().decode(INVALID_BASE_64_URL_USERNAME_HASH);
|
||||
private static final byte[] TOO_SHORT_USERNAME_HASH = Base64.getUrlDecoder().decode(TOO_SHORT_BASE_64_URL_USERNAME_HASH);
|
||||
|
||||
private static final UUID SENDER_REG_LOCK_UUID = UUID.randomUUID();
|
||||
private static final UUID SENDER_TRANSFER_UUID = UUID.randomUUID();
|
||||
private static final UUID RESERVATION_TOKEN = UUID.randomUUID();
|
||||
|
||||
private static final String NICE_HOST = "127.0.0.1";
|
||||
private static final String RATE_LIMITED_IP_HOST = "10.0.0.1";
|
||||
@@ -186,6 +193,7 @@ class AccountControllerTest {
|
||||
new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class,
|
||||
DisabledPermittedAuthenticatedAccount.class)))
|
||||
.addProvider(new JsonMappingExceptionMapper())
|
||||
.addProvider(new RateLimitExceededExceptionMapper())
|
||||
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
|
||||
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
||||
@@ -272,9 +280,6 @@ class AccountControllerTest {
|
||||
return account;
|
||||
});
|
||||
|
||||
when(accountsManager.setUsername(AuthHelper.VALID_ACCOUNT, "takenusername", null))
|
||||
.thenThrow(new UsernameNotAvailableException());
|
||||
|
||||
when(changeNumberManager.changeNumber(any(), any(), any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
|
||||
final Account account = invocation.getArgument(0, Account.class);
|
||||
final String number = invocation.getArgument(1, String.class);
|
||||
@@ -1648,143 +1653,140 @@ class AccountControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsername() throws UsernameNotAvailableException {
|
||||
Account account = mock(Account.class);
|
||||
when(account.getUsername()).thenReturn(Optional.of("N00bkilleR.1234"));
|
||||
when(accountsManager.setUsername(any(), eq("N00bkilleR"), isNull()))
|
||||
.thenReturn(account);
|
||||
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
|
||||
when(accountsManager.reserveUsernameHash(any(), any()))
|
||||
.thenReturn(new AccountsManager.UsernameReservation(null, USERNAME_HASH_1));
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new UsernameRequest("N00bkilleR", null)));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.readEntity(UsernameResponse.class).username()).isEqualTo("N00bkilleR.1234");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReserveUsername() throws UsernameNotAvailableException {
|
||||
when(accountsManager.reserveUsername(any(), eq("N00bkilleR")))
|
||||
.thenReturn(new AccountsManager.UsernameReservation(null, "N00bkilleR.1234", RESERVATION_TOKEN));
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username/reserved")
|
||||
.target("/v1/accounts/username_hash/reserve")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new ReserveUsernameRequest("N00bkilleR")));
|
||||
.put(Entity.json(new ReserveUsernameHashRequest(List.of(USERNAME_HASH_1, USERNAME_HASH_2))));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.readEntity(ReserveUsernameResponse.class))
|
||||
.satisfies(r -> r.username().equals("N00bkilleR.1234"))
|
||||
.satisfies(r -> r.reservationToken().equals(RESERVATION_TOKEN));
|
||||
assertThat(response.readEntity(ReserveUsernameHashResponse.class))
|
||||
.satisfies(r -> assertThat(r.usernameHash()).hasSize(32));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommitUsername() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
|
||||
Account account = mock(Account.class);
|
||||
when(account.getUsername()).thenReturn(Optional.of("n00bkiller.1234"));
|
||||
when(accountsManager.confirmReservedUsername(any(), eq("n00bkiller.1234"), eq(RESERVATION_TOKEN))).thenReturn(account);
|
||||
void testReserveUsernameHashUnavailable() throws UsernameHashNotAvailableException {
|
||||
when(accountsManager.reserveUsernameHash(any(), anyList()))
|
||||
.thenThrow(new UsernameHashNotAvailableException());
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username/confirm")
|
||||
.target("/v1/accounts/username_hash/reserve")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new ConfirmUsernameRequest("n00bkiller.1234", RESERVATION_TOKEN)));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.readEntity(UsernameResponse.class).username()).isEqualTo("n00bkiller.1234");
|
||||
.put(Entity.json(new ReserveUsernameHashRequest(List.of(USERNAME_HASH_1, USERNAME_HASH_2))));
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testReserveUsernameHashListSizeInvalid(List<byte[]> usernameHashes) {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username_hash/reserve")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new ReserveUsernameHashRequest(usernameHashes)));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
static Stream<Arguments> testReserveUsernameHashListSizeInvalid() {
|
||||
return Stream.of(
|
||||
Arguments.of(Collections.nCopies(21, USERNAME_HASH_1)),
|
||||
Arguments.of(Collections.emptyList())
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommitUnreservedUsername() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
|
||||
when(accountsManager.confirmReservedUsername(any(), eq("n00bkiller.1234"), eq(RESERVATION_TOKEN)))
|
||||
void testReserveUsernameHashInvalidHashSize() {
|
||||
List<byte[]> usernameHashes = List.of(new byte[31]);
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username_hash/reserve")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new ReserveUsernameHashRequest(usernameHashes)));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReserveUsernameHashInvalidBase64UrlEncoding() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username_hash/reserve")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(
|
||||
// Has '+' and '='characters which are invalid in base64url
|
||||
"""
|
||||
{
|
||||
"usernameHashes": ["jh1jJ50oGn9wUXAFNtDus6AJgWOQ6XbZzF+wCv7OOQs="]
|
||||
}
|
||||
"""));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommitUsername() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||
Account account = mock(Account.class);
|
||||
when(account.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH_1));
|
||||
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1))).thenReturn(account);
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username_hash/confirm")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1)));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertArrayEquals(response.readEntity(UsernameHashResponse.class).usernameHash(), USERNAME_HASH_1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommitUnreservedUsername() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1)))
|
||||
.thenThrow(new UsernameReservationNotFoundException());
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username/confirm")
|
||||
.target("/v1/accounts/username_hash/confirm")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new ConfirmUsernameRequest("n00bkiller.1234", RESERVATION_TOKEN)));
|
||||
.put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1)));
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommitLapsedUsername() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
|
||||
when(accountsManager.confirmReservedUsername(any(), eq("n00bkiller.1234"), eq(RESERVATION_TOKEN)))
|
||||
.thenThrow(new UsernameNotAvailableException());
|
||||
void testCommitLapsedUsername() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1)))
|
||||
.thenThrow(new UsernameHashNotAvailableException());
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username/confirm")
|
||||
.target("/v1/accounts/username_hash/confirm")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new ConfirmUsernameRequest("n00bkiller.1234", RESERVATION_TOKEN)));
|
||||
.put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1)));
|
||||
assertThat(response.getStatus()).isEqualTo(410);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetTakenUsername() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new UsernameRequest("takenusername", null)));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetInvalidUsername() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
// contains non-ascii character
|
||||
.put(Entity.json(new UsernameRequest("pаypal", null)));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetInvalidPrefixUsername() throws JsonProcessingException {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new UsernameRequest("0n00bkiller", null)));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameBadAuth() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.put(Entity.json(new UsernameRequest("n00bkiller", null)));
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUsername() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username/")
|
||||
.target("/v1/accounts/username_hash/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.delete();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
verify(accountsManager).clearUsername(AuthHelper.VALID_ACCOUNT);
|
||||
verify(accountsManager).clearUsernameHash(AuthHelper.VALID_ACCOUNT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUsernameBadAuth() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/username/")
|
||||
.target("/v1/accounts/username_hash/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.delete();
|
||||
@@ -1998,9 +2000,9 @@ class AccountControllerTest {
|
||||
final UUID uuid = UUID.randomUUID();
|
||||
when(account.getUuid()).thenReturn(uuid);
|
||||
|
||||
when(accountsManager.getByUsername(eq("n00bkiller.1234"))).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByUsernameHash(any())).thenReturn(Optional.of(account));
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("v1/accounts/username/n00bkiller.1234")
|
||||
.target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1))
|
||||
.request()
|
||||
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
|
||||
.get();
|
||||
@@ -2010,9 +2012,9 @@ class AccountControllerTest {
|
||||
|
||||
@Test
|
||||
void testLookupUsernameDoesNotExist() {
|
||||
when(accountsManager.getByUsername(eq("n00bkiller.1234"))).thenReturn(Optional.empty());
|
||||
when(accountsManager.getByUsernameHash(any())).thenReturn(Optional.empty());
|
||||
assertThat(resources.getJerseyTest()
|
||||
.target("v1/accounts/username/n00bkiller.1234")
|
||||
.target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1))
|
||||
.request()
|
||||
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
|
||||
.get().getStatus()).isEqualTo(404);
|
||||
@@ -2024,7 +2026,7 @@ class AccountControllerTest {
|
||||
MockUtils.updateRateLimiterResponseToFail(
|
||||
rateLimiters, RateLimiters.Handle.USERNAME_LOOKUP, "127.0.0.1", expectedRetryAfter);
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("/v1/accounts/username/test.123")
|
||||
.target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1))
|
||||
.request()
|
||||
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
|
||||
.get();
|
||||
@@ -2033,6 +2035,34 @@ class AccountControllerTest {
|
||||
assertThat(response.getHeaderString("Retry-After")).isEqualTo(String.valueOf(expectedRetryAfter.toSeconds()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLookupUsernameAuthenticated() {
|
||||
assertThat(resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/username_hash/%s", USERNAME_HASH_1))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
|
||||
.get()
|
||||
.getStatus()).isEqualTo(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLookupUsernameInvalidFormat() {
|
||||
assertThat(resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/username_hash/%s", INVALID_USERNAME_HASH))
|
||||
.request()
|
||||
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
|
||||
.get()
|
||||
.getStatus()).isEqualTo(422);
|
||||
|
||||
assertThat(resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/username_hash/%s", TOO_SHORT_USERNAME_HASH))
|
||||
.request()
|
||||
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
|
||||
.get()
|
||||
.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void pushTokensMatch(@Nullable final String pushChallenge, @Nullable final StoredVerificationCode storedVerificationCode, final boolean expectMatch) {
|
||||
|
||||
@@ -31,7 +31,6 @@ import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -132,6 +131,8 @@ class ProfileControllerTest {
|
||||
private static final String ACCOUNT_PHONE_NUMBER_IDENTITY_KEY = "bazz";
|
||||
private static final String ACCOUNT_TWO_IDENTITY_KEY = "bar";
|
||||
private static final String ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY = "baz";
|
||||
private static final String BASE_64_URL_USERNAME_HASH = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
|
||||
private static final byte[] USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH);
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(
|
||||
DynamicConfigurationManager.class);
|
||||
@@ -197,7 +198,7 @@ class ProfileControllerTest {
|
||||
when(profileAccount.isAnnouncementGroupSupported()).thenReturn(false);
|
||||
when(profileAccount.isChangeNumberSupported()).thenReturn(false);
|
||||
when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.empty());
|
||||
when(profileAccount.getUsername()).thenReturn(Optional.of("n00bkiller"));
|
||||
when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH));
|
||||
when(profileAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes()));
|
||||
|
||||
Account capabilitiesAccount = mock(Account.class);
|
||||
@@ -212,7 +213,7 @@ class ProfileControllerTest {
|
||||
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount));
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID_TWO)).thenReturn(Optional.of(profileAccount));
|
||||
when(accountsManager.getByPhoneNumberIdentifier(AuthHelper.VALID_PNI_TWO)).thenReturn(Optional.of(profileAccount));
|
||||
when(accountsManager.getByUsername("n00bkiller")).thenReturn(Optional.of(profileAccount));
|
||||
when(accountsManager.getByUsernameHash(USERNAME_HASH)).thenReturn(Optional.of(profileAccount));
|
||||
|
||||
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount));
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(capabilitiesAccount));
|
||||
|
||||
@@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
@@ -26,8 +27,10 @@ class AccountChangeValidatorTest {
|
||||
private static final UUID ORIGINAL_PNI = UUID.randomUUID();
|
||||
private static final UUID CHANGED_PNI = UUID.randomUUID();
|
||||
|
||||
private static final String ORIGINAL_USERNAME = "bruce_wayne";
|
||||
private static final String CHANGED_USERNAME = "batman";
|
||||
private static final String BASE_64_URL_ORIGINAL_USERNAME = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
|
||||
private static final String BASE_64_URL_CHANGED_USERNAME = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
|
||||
private static final byte[] ORIGINAL_USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_ORIGINAL_USERNAME);
|
||||
private static final byte[] CHANGED_USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_CHANGED_USERNAME);
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
@@ -49,22 +52,22 @@ class AccountChangeValidatorTest {
|
||||
final Account originalAccount = mock(Account.class);
|
||||
when(originalAccount.getNumber()).thenReturn(ORIGINAL_NUMBER);
|
||||
when(originalAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI);
|
||||
when(originalAccount.getUsername()).thenReturn(Optional.of(ORIGINAL_USERNAME));
|
||||
when(originalAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH));
|
||||
|
||||
final Account unchangedAccount = mock(Account.class);
|
||||
when(unchangedAccount.getNumber()).thenReturn(ORIGINAL_NUMBER);
|
||||
when(unchangedAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI);
|
||||
when(unchangedAccount.getUsername()).thenReturn(Optional.of(ORIGINAL_USERNAME));
|
||||
when(unchangedAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH));
|
||||
|
||||
final Account changedNumberAccount = mock(Account.class);
|
||||
when(changedNumberAccount.getNumber()).thenReturn(CHANGED_NUMBER);
|
||||
when(changedNumberAccount.getPhoneNumberIdentifier()).thenReturn(CHANGED_PNI);
|
||||
when(changedNumberAccount.getUsername()).thenReturn(Optional.of(ORIGINAL_USERNAME));
|
||||
when(changedNumberAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH));
|
||||
|
||||
final Account changedUsernameAccount = mock(Account.class);
|
||||
when(changedUsernameAccount.getNumber()).thenReturn(ORIGINAL_NUMBER);
|
||||
when(changedUsernameAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI);
|
||||
when(changedUsernameAccount.getUsername()).thenReturn(Optional.of(CHANGED_USERNAME));
|
||||
when(changedUsernameAccount.getUsernameHash()).thenReturn(Optional.of(CHANGED_USERNAME_HASH));
|
||||
|
||||
return Stream.of(
|
||||
Arguments.of(originalAccount, unchangedAccount, AccountChangeValidator.GENERAL_CHANGE_VALIDATOR, true),
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
|
||||
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex;
|
||||
@@ -192,13 +191,11 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
mock(DirectoryQueue.class),
|
||||
mock(Keys.class),
|
||||
mock(MessagesManager.class),
|
||||
mock(ProhibitedUsernames.class),
|
||||
mock(ProfilesManager.class),
|
||||
mock(StoredVerificationCodeManager.class),
|
||||
secureStorageClient,
|
||||
secureBackupClient,
|
||||
clientPresenceManager,
|
||||
mock(UsernameGenerator.class),
|
||||
mock(ExperimentEnrollmentManager.class),
|
||||
mock(Clock.class));
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ import org.whispersystems.textsecuregcm.tests.util.DevicesHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.JsonHelpers;
|
||||
import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
|
||||
@@ -159,13 +158,11 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
mock(DirectoryQueue.class),
|
||||
mock(Keys.class),
|
||||
mock(MessagesManager.class),
|
||||
mock(ProhibitedUsernames.class),
|
||||
mock(ProfilesManager.class),
|
||||
mock(StoredVerificationCodeManager.class),
|
||||
mock(SecureStorageClient.class),
|
||||
mock(SecureBackupClient.class),
|
||||
mock(ClientPresenceManager.class),
|
||||
mock(UsernameGenerator.class),
|
||||
mock(ExperimentEnrollmentManager.class),
|
||||
mock(Clock.class)
|
||||
);
|
||||
|
||||
@@ -5,18 +5,15 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.AdditionalMatchers.and;
|
||||
import static org.mockito.AdditionalMatchers.not;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.startsWith;
|
||||
import static org.mockito.Mockito.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
@@ -31,15 +28,17 @@ import static org.mockito.Mockito.when;
|
||||
import io.lettuce.core.RedisException;
|
||||
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -47,9 +46,7 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.whispersystems.textsecuregcm.configuration.UsernameConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
@@ -62,10 +59,12 @@ import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
|
||||
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.UsernameNormalizer;
|
||||
|
||||
class AccountsManagerTest {
|
||||
private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
|
||||
private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
|
||||
private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1);
|
||||
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
|
||||
|
||||
private Accounts accounts;
|
||||
private DeletedAccountsManager deletedAccountsManager;
|
||||
@@ -73,7 +72,6 @@ class AccountsManagerTest {
|
||||
private Keys keys;
|
||||
private MessagesManager messagesManager;
|
||||
private ProfilesManager profilesManager;
|
||||
private ProhibitedUsernames prohibitedUsernames;
|
||||
private ExperimentEnrollmentManager enrollmentManager;
|
||||
|
||||
private Map<String, UUID> phoneNumberIdentifiersByE164;
|
||||
@@ -89,8 +87,6 @@ class AccountsManagerTest {
|
||||
return null;
|
||||
};
|
||||
|
||||
private static final UUID RESERVATION_TOKEN = UUID.randomUUID();
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws InterruptedException {
|
||||
accounts = mock(Accounts.class);
|
||||
@@ -99,7 +95,6 @@ class AccountsManagerTest {
|
||||
keys = mock(Keys.class);
|
||||
messagesManager = mock(MessagesManager.class);
|
||||
profilesManager = mock(ProfilesManager.class);
|
||||
prohibitedUsernames = mock(ProhibitedUsernames.class);
|
||||
|
||||
//noinspection unchecked
|
||||
commands = mock(RedisAdvancedClusterCommands.class);
|
||||
@@ -143,7 +138,7 @@ class AccountsManagerTest {
|
||||
|
||||
enrollmentManager = mock(ExperimentEnrollmentManager.class);
|
||||
when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true);
|
||||
when(accounts.usernameAvailable(any())).thenReturn(true);
|
||||
when(accounts.usernameHashAvailable(any())).thenReturn(true);
|
||||
|
||||
accountsManager = new AccountsManager(
|
||||
accounts,
|
||||
@@ -153,13 +148,11 @@ class AccountsManagerTest {
|
||||
directoryQueue,
|
||||
keys,
|
||||
messagesManager,
|
||||
prohibitedUsernames,
|
||||
profilesManager,
|
||||
mock(StoredVerificationCodeManager.class),
|
||||
storageClient,
|
||||
backupClient,
|
||||
mock(ClientPresenceManager.class),
|
||||
new UsernameGenerator(new UsernameConfiguration()),
|
||||
enrollmentManager,
|
||||
mock(Clock.class));
|
||||
}
|
||||
@@ -169,7 +162,8 @@ class AccountsManagerTest {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(uuid.toString());
|
||||
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
|
||||
when(commands.get(eq("Account3::" + uuid))).thenReturn(
|
||||
"{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
|
||||
|
||||
Optional<Account> account = accountsManager.getByE164("+14152222222");
|
||||
|
||||
@@ -188,7 +182,8 @@ class AccountsManagerTest {
|
||||
void testGetAccountByUuidInCache() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
|
||||
when(commands.get(eq("Account3::" + uuid))).thenReturn(
|
||||
"{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
|
||||
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
||||
|
||||
@@ -209,7 +204,8 @@ class AccountsManagerTest {
|
||||
UUID pni = UUID.randomUUID();
|
||||
|
||||
when(commands.get(eq("AccountMap::" + pni))).thenReturn(uuid.toString());
|
||||
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
|
||||
when(commands.get(eq("Account3::" + uuid))).thenReturn(
|
||||
"{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
|
||||
|
||||
Optional<Account> account = accountsManager.getByPhoneNumberIdentifier(pni);
|
||||
|
||||
@@ -225,21 +221,21 @@ class AccountsManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetByUsernameInCache() {
|
||||
void testGetByUsernameHashInCache() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String username = "test";
|
||||
when(commands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))).thenReturn(uuid.toString());
|
||||
when(commands.get(eq("Account3::" + uuid))).thenReturn(
|
||||
String.format("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\", \"usernameHash\": \"%s\"}",
|
||||
BASE_64_URL_USERNAME_HASH_1));
|
||||
|
||||
when(commands.get(eq("UAccountMap::" + username))).thenReturn(uuid.toString());
|
||||
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\", \"username\": \"test\"}");
|
||||
|
||||
Optional<Account> account = accountsManager.getByUsername(username);
|
||||
Optional<Account> account = accountsManager.getByUsernameHash(USERNAME_HASH_1);
|
||||
|
||||
assertTrue(account.isPresent());
|
||||
assertEquals(account.get().getNumber(), "+14152222222");
|
||||
assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier());
|
||||
assertEquals(Optional.of(username), account.get().getUsername());
|
||||
assertArrayEquals(USERNAME_HASH_1, account.get().getUsernameHash().get());
|
||||
|
||||
verify(commands).get(eq("UAccountMap::" + username));
|
||||
verify(commands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1));
|
||||
verify(commands).get(eq("Account3::" + uuid));
|
||||
verifyNoMoreInteractions(commands);
|
||||
|
||||
@@ -320,29 +316,28 @@ class AccountsManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAccountByUsernameNotInCache() {
|
||||
void testGetAccountByUsernameHashNotInCache() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String username = "test";
|
||||
|
||||
Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
account.setUsername(username);
|
||||
account.setUsernameHash(USERNAME_HASH_1);
|
||||
|
||||
when(commands.get(eq("UAccountMap::" + username))).thenReturn(null);
|
||||
when(accounts.getByUsername(username)).thenReturn(Optional.of(account));
|
||||
when(commands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))).thenReturn(null);
|
||||
when(accounts.getByUsernameHash(USERNAME_HASH_1)).thenReturn(Optional.of(account));
|
||||
|
||||
Optional<Account> retrieved = accountsManager.getByUsername(username);
|
||||
Optional<Account> retrieved = accountsManager.getByUsernameHash(USERNAME_HASH_1);
|
||||
|
||||
assertTrue(retrieved.isPresent());
|
||||
assertSame(retrieved.get(), account);
|
||||
|
||||
verify(commands).get(eq("UAccountMap::" + username));
|
||||
verify(commands).setex(eq("UAccountMap::" + username), anyLong(), eq(uuid.toString()));
|
||||
verify(commands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1));
|
||||
verify(commands).setex(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1), anyLong(), eq(uuid.toString()));
|
||||
verify(commands).setex(eq("AccountMap::" + account.getPhoneNumberIdentifier()), anyLong(), eq(uuid.toString()));
|
||||
verify(commands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString()));
|
||||
verify(commands).setex(eq("Account3::" + uuid), anyLong(), anyString());
|
||||
verifyNoMoreInteractions(commands);
|
||||
|
||||
verify(accounts).getByUsername(username);
|
||||
verify(accounts).getByUsernameHash(USERNAME_HASH_1);
|
||||
verifyNoMoreInteractions(accounts);
|
||||
}
|
||||
|
||||
@@ -422,27 +417,26 @@ class AccountsManagerTest {
|
||||
@Test
|
||||
void testGetAccountByUsernameBrokenCache() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String username = "test";
|
||||
|
||||
Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
account.setUsername(username);
|
||||
account.setUsernameHash(USERNAME_HASH_1);
|
||||
|
||||
when(commands.get(eq("UAccountMap::" + username))).thenThrow(new RedisException("OH NO"));
|
||||
when(accounts.getByUsername(username)).thenReturn(Optional.of(account));
|
||||
when(commands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))).thenThrow(new RedisException("OH NO"));
|
||||
when(accounts.getByUsernameHash(USERNAME_HASH_1)).thenReturn(Optional.of(account));
|
||||
|
||||
Optional<Account> retrieved = accountsManager.getByUsername(username);
|
||||
Optional<Account> retrieved = accountsManager.getByUsernameHash(USERNAME_HASH_1);
|
||||
|
||||
assertTrue(retrieved.isPresent());
|
||||
assertSame(retrieved.get(), account);
|
||||
|
||||
verify(commands).get(eq("UAccountMap::" + username));
|
||||
verify(commands).setex(eq("UAccountMap::" + username), anyLong(), eq(uuid.toString()));
|
||||
verify(commands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1));
|
||||
verify(commands).setex(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1), anyLong(), eq(uuid.toString()));
|
||||
verify(commands).setex(eq("AccountMap::" + account.getPhoneNumberIdentifier()), anyLong(), eq(uuid.toString()));
|
||||
verify(commands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString()));
|
||||
verify(commands).setex(eq("Account3::" + uuid), anyLong(), anyString());
|
||||
verifyNoMoreInteractions(commands);
|
||||
|
||||
verify(accounts).getByUsername(username);
|
||||
verify(accounts).getByUsernameHash(USERNAME_HASH_1);
|
||||
verifyNoMoreInteractions(accounts);
|
||||
}
|
||||
|
||||
@@ -734,188 +728,96 @@ class AccountsManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsername() {
|
||||
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String nickname = "test";
|
||||
assertDoesNotThrow(() -> accountsManager.setUsername(account, nickname, null));
|
||||
verify(accounts).setUsername(eq(account), startsWith(nickname));
|
||||
final List<byte[]> usernameHashes = List.of(new byte[32], new byte[32]);
|
||||
when(accounts.usernameHashAvailable(any())).thenReturn(true);
|
||||
accountsManager.reserveUsernameHash(account, usernameHashes);
|
||||
verify(accounts).reserveUsernameHash(eq(account), eq(new byte[32]), eq(Duration.ofMinutes(5)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReserveUsername() throws UsernameNotAvailableException {
|
||||
void testReserveUsernameHashNotAvailable() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String nickname = "beethoven";
|
||||
accountsManager.reserveUsername(account, nickname);
|
||||
verify(accounts).reserveUsername(eq(account), startsWith(nickname), any());
|
||||
when(accounts.usernameHashAvailable(any())).thenReturn(false);
|
||||
|
||||
assertThrows(UsernameHashNotAvailableException.class, () -> accountsManager.reserveUsernameHash(account, List.of(
|
||||
USERNAME_HASH_1, USERNAME_HASH_2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetReservedUsername() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
|
||||
void testReserveUsernameDisabled() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String reserved = "sCoObY.1234";
|
||||
setReservationHash(account, reserved);
|
||||
when(accounts.usernameAvailable(eq(Optional.of(RESERVATION_TOKEN)), eq(reserved))).thenReturn(true);
|
||||
accountsManager.confirmReservedUsername(account, reserved, RESERVATION_TOKEN);
|
||||
verify(accounts).confirmUsername(eq(account), eq(reserved), eq(RESERVATION_TOKEN));
|
||||
when(enrollmentManager.isEnrolled(account.getUuid(), AccountsManager.USERNAME_EXPERIMENT_NAME)).thenReturn(false);
|
||||
assertThrows(UsernameHashNotAvailableException.class, () -> accountsManager.reserveUsernameHash(account, List.of(
|
||||
USERNAME_HASH_1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetReservedHashNameMismatch() {
|
||||
void testConfirmReservedUsernameHash() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
setReservationHash(account, "pluto.1234");
|
||||
when(accounts.usernameAvailable(eq(Optional.of(RESERVATION_TOKEN)), eq("pluto.1234"))).thenReturn(true);
|
||||
setReservationHash(account, USERNAME_HASH_1);
|
||||
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))).thenReturn(true);
|
||||
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1);
|
||||
verify(accounts).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfirmReservedHashNameMismatch() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
setReservationHash(account, USERNAME_HASH_1);
|
||||
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))).thenReturn(true);
|
||||
assertThrows(UsernameReservationNotFoundException.class,
|
||||
() -> accountsManager.confirmReservedUsername(account, "goofy.1234", RESERVATION_TOKEN));
|
||||
() -> accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetReservedHashAciMismatch() {
|
||||
void testConfirmReservedLapsed() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String reserved = "toto.1234";
|
||||
account.setReservedUsernameHash(Accounts.reservedUsernameHash(UUID.randomUUID(), reserved));
|
||||
when(accounts.usernameAvailable(eq(Optional.of(RESERVATION_TOKEN)), eq(reserved))).thenReturn(true);
|
||||
assertThrows(UsernameReservationNotFoundException.class,
|
||||
() -> accountsManager.confirmReservedUsername(account, reserved, RESERVATION_TOKEN));
|
||||
// hash was reserved, but the reservation lapsed and another account took it
|
||||
setReservationHash(account, USERNAME_HASH_1);
|
||||
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))).thenReturn(false);
|
||||
assertThrows(UsernameHashNotAvailableException.class, () -> accountsManager.confirmReservedUsernameHash(account,
|
||||
USERNAME_HASH_1));
|
||||
verify(accounts, never()).confirmUsernameHash(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetReservedLapsed() {
|
||||
void testConfirmReservedRetry() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String reserved = "porkchop.1234";
|
||||
// name was reserved, but the reservation lapsed and another account took it
|
||||
setReservationHash(account, reserved);
|
||||
when(accounts.usernameAvailable(eq(Optional.of(RESERVATION_TOKEN)), eq(reserved))).thenReturn(false);
|
||||
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.confirmReservedUsername(account, reserved, RESERVATION_TOKEN));
|
||||
verify(accounts, never()).confirmUsername(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetReservedRetry() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String username = "santaslittlehelper.1234";
|
||||
account.setUsername(username);
|
||||
account.setUsernameHash(USERNAME_HASH_1);
|
||||
|
||||
// reserved username already set, should be treated as a replay
|
||||
accountsManager.confirmReservedUsername(account, username, RESERVATION_TOKEN);
|
||||
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1);
|
||||
verifyNoInteractions(accounts);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameSameUsername() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String nickname = "test";
|
||||
account.setUsername(nickname + ".123");
|
||||
|
||||
// should be treated as a replayed request
|
||||
assertDoesNotThrow(() -> accountsManager.setUsername(account, nickname, null));
|
||||
verify(accounts, never()).setUsername(eq(account), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameReroll() throws UsernameNotAvailableException {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String nickname = "test";
|
||||
final String username = nickname + ".ZZZ";
|
||||
account.setUsername(username);
|
||||
|
||||
// given the correct old username, should reroll discriminator even if the nick matches
|
||||
accountsManager.setUsername(account, nickname, username);
|
||||
verify(accounts).setUsername(eq(account), and(startsWith(nickname), not(eq(username))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReserveUsernameReroll() throws UsernameNotAvailableException {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String nickname = "clifford";
|
||||
final String username = nickname + ".ZZZ";
|
||||
account.setUsername(username);
|
||||
|
||||
// given the correct old username, should reroll discriminator even if the nick matches
|
||||
accountsManager.reserveUsername(account, nickname);
|
||||
verify(accounts).reserveUsername(eq(account), and(startsWith(nickname), not(eq(username))), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetReservedUsernameWithNoReservation() {
|
||||
void testConfirmReservedUsernameHashWithNoReservation() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
|
||||
new ArrayList<>(), new byte[16]);
|
||||
assertThrows(UsernameReservationNotFoundException.class,
|
||||
() -> accountsManager.confirmReservedUsername(account, "laika.1234", RESERVATION_TOKEN));
|
||||
verify(accounts, never()).confirmUsername(any(), any(), any());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testUsernameExpandDiscriminator(boolean reserve) throws UsernameNotAvailableException {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String nickname = "test";
|
||||
|
||||
ArgumentMatcher<String> isWide = (String username) -> {
|
||||
String[] spl = username.split(Pattern.quote(UsernameGenerator.SEPARATOR));
|
||||
assertEquals(spl.length, 2);
|
||||
int discriminator = Integer.parseInt(spl[1]);
|
||||
// require a 7 digit discriminator
|
||||
return discriminator > 1_000_000;
|
||||
};
|
||||
when(accounts.usernameAvailable(any())).thenReturn(false);
|
||||
when(accounts.usernameAvailable(argThat(isWide))).thenReturn(true);
|
||||
|
||||
if (reserve) {
|
||||
accountsManager.reserveUsername(account, nickname);
|
||||
verify(accounts).reserveUsername(eq(account), and(startsWith(nickname), argThat(isWide)), any());
|
||||
|
||||
} else {
|
||||
accountsManager.setUsername(account, nickname, null);
|
||||
verify(accounts).setUsername(eq(account), and(startsWith(nickname), argThat(isWide)));
|
||||
}
|
||||
() -> accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1));
|
||||
verify(accounts, never()).confirmUsernameHash(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChangeUsername() throws UsernameNotAvailableException {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String nickname = "test";
|
||||
account.setUsername("old.123");
|
||||
accountsManager.setUsername(account, nickname, "old.123");
|
||||
verify(accounts).setUsername(eq(account), startsWith(nickname));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameNotAvailable() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
final String nickname = "unavailable";
|
||||
when(accounts.usernameAvailable(startsWith(nickname))).thenReturn(false);
|
||||
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.setUsername(account, nickname, null));
|
||||
verify(accounts, never()).setUsername(any(), any());
|
||||
assertTrue(account.getUsername().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameReserved() {
|
||||
final String nickname = "reserved";
|
||||
when(prohibitedUsernames.isProhibited(eq(nickname), any())).thenReturn(true);
|
||||
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
|
||||
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.setUsername(account, nickname, null));
|
||||
assertTrue(account.getUsername().isEmpty());
|
||||
void testClearUsernameHash() {
|
||||
Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
account.setUsernameHash(USERNAME_HASH_1);
|
||||
accountsManager.clearUsernameHash(account);
|
||||
verify(accounts).clearUsernameHash(eq(account));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameViaUpdate() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
|
||||
assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setUsername("test")));
|
||||
assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setUsernameHash(USERNAME_HASH_1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameDisabled() {
|
||||
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
|
||||
when(enrollmentManager.isEnrolled(account.getUuid(), AccountsManager.USERNAME_EXPERIMENT_NAME)).thenReturn(false);
|
||||
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.setUsername(account, "n00bkiller", null));
|
||||
}
|
||||
|
||||
private void setReservationHash(final Account account, final String reservedUsername) {
|
||||
account.setReservedUsernameHash(Accounts.reservedUsernameHash(account.getUuid(), reservedUsername));
|
||||
private void setReservationHash(final Account account, final byte[] reservedUsernameHash) {
|
||||
account.setReservedUsernameHash(reservedUsernameHash);
|
||||
}
|
||||
|
||||
private static Device generateTestDevice(final long lastSeen) {
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.argThat;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.eq;
|
||||
@@ -17,11 +18,15 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -29,8 +34,6 @@ import java.util.function.Consumer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
@@ -42,7 +45,6 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
|
||||
@@ -59,7 +61,11 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
private static final String PNI_ASSIGNMENT_TABLE_NAME = "pni_assignment_test";
|
||||
private static final String USERNAMES_TABLE_NAME = "usernames_test";
|
||||
private static final String PNI_TABLE_NAME = "pni_test";
|
||||
private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
|
||||
private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
|
||||
private static final int SCAN_PAGE_SIZE = 1;
|
||||
private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1);
|
||||
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
|
||||
|
||||
@RegisterExtension
|
||||
static DynamoDbExtension ACCOUNTS_DYNAMO_EXTENSION = DynamoDbExtension.builder()
|
||||
@@ -86,7 +92,6 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
|
||||
private AccountsManager accountsManager;
|
||||
private Accounts accounts;
|
||||
private UsernameGenerator usernameGenerator;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws InterruptedException {
|
||||
@@ -107,12 +112,12 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
CreateTableRequest createUsernamesTableRequest = CreateTableRequest.builder()
|
||||
.tableName(USERNAMES_TABLE_NAME)
|
||||
.keySchema(KeySchemaElement.builder()
|
||||
.attributeName(Accounts.ATTR_USERNAME)
|
||||
.attributeName(Accounts.ATTR_USERNAME_HASH)
|
||||
.keyType(KeyType.HASH)
|
||||
.build())
|
||||
.attributeDefinitions(AttributeDefinition.builder()
|
||||
.attributeName(Accounts.ATTR_USERNAME)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.attributeName(Accounts.ATTR_USERNAME_HASH)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
|
||||
.build();
|
||||
@@ -152,8 +157,6 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
USERNAMES_TABLE_NAME,
|
||||
SCAN_PAGE_SIZE));
|
||||
|
||||
usernameGenerator = new UsernameGenerator(initialWidth, discriminatorMaxWidth, attemptsPerWidth,
|
||||
Duration.ofDays(1));
|
||||
final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
|
||||
doAnswer((final InvocationOnMock invocationOnMock) -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -176,211 +179,159 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
mock(DirectoryQueue.class),
|
||||
mock(Keys.class),
|
||||
mock(MessagesManager.class),
|
||||
mock(ProhibitedUsernames.class),
|
||||
mock(ProfilesManager.class),
|
||||
mock(StoredVerificationCodeManager.class),
|
||||
mock(SecureStorageClient.class),
|
||||
mock(SecureBackupClient.class),
|
||||
mock(ClientPresenceManager.class),
|
||||
usernameGenerator,
|
||||
experimentEnrollmentManager,
|
||||
mock(Clock.class));
|
||||
}
|
||||
|
||||
private static int discriminator(String username) {
|
||||
return Integer.parseInt(username.substring(username.indexOf(UsernameGenerator.SEPARATOR) + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetClearUsername() throws UsernameNotAvailableException, InterruptedException {
|
||||
void testNoUsernames() throws InterruptedException {
|
||||
Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
|
||||
new ArrayList<>());
|
||||
account = accountsManager.setUsername(account, "n00bkiller", null);
|
||||
assertThat(account.getUsername()).isPresent();
|
||||
assertThat(account.getUsername().get()).startsWith("n00bkiller");
|
||||
int discriminator = discriminator(account.getUsername().get());
|
||||
assertThat(discriminator).isGreaterThan(0).isLessThan(10);
|
||||
|
||||
assertThat(accountsManager.getByUsername(account.getUsername().get()).orElseThrow().getUuid()).isEqualTo(
|
||||
account.getUuid());
|
||||
|
||||
// reroll
|
||||
account = accountsManager.setUsername(account, "n00bkiller", account.getUsername().get());
|
||||
final String newUsername = account.getUsername().orElseThrow();
|
||||
assertThat(discriminator(account.getUsername().orElseThrow())).isNotEqualTo(discriminator);
|
||||
|
||||
// clear
|
||||
account = accountsManager.clearUsername(account);
|
||||
assertThat(accountsManager.getByUsername(newUsername)).isEmpty();
|
||||
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsername()).isEmpty();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testNoUsernames(boolean reserve) throws InterruptedException {
|
||||
Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
|
||||
new ArrayList<>());
|
||||
for (int i = 1; i <= 99; i++) {
|
||||
List<byte[]> usernameHashes = List.of(USERNAME_HASH_1, USERNAME_HASH_2);
|
||||
int i = 0;
|
||||
for (byte[] hash : usernameHashes) {
|
||||
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
|
||||
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(UUID.randomUUID()),
|
||||
Accounts.ATTR_USERNAME, AttributeValues.fromString(usernameGenerator.fromParts("n00bkiller", i))));
|
||||
Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(hash)));
|
||||
// half of these are taken usernames, half are only reservations (have a TTL)
|
||||
if (i % 2 == 0) {
|
||||
item.put(Accounts.ATTR_TTL,
|
||||
AttributeValues.fromLong(Instant.now().plus(Duration.ofMinutes(1)).getEpochSecond()));
|
||||
}
|
||||
i++;
|
||||
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder()
|
||||
.tableName(USERNAMES_TABLE_NAME)
|
||||
.item(item)
|
||||
.build());
|
||||
}
|
||||
assertThrows(UsernameNotAvailableException.class, () -> {
|
||||
if (reserve) {
|
||||
accountsManager.reserveUsername(account, "n00bkiller");
|
||||
} else {
|
||||
accountsManager.setUsername(account, "n00bkiller", null);
|
||||
}
|
||||
});
|
||||
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsername()).isEmpty();
|
||||
assertThrows(UsernameHashNotAvailableException.class, () -> {accountsManager.reserveUsernameHash(account, usernameHashes);});
|
||||
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUsernameSnatched() throws InterruptedException, UsernameNotAvailableException {
|
||||
void testReserveUsernameSnatched() throws InterruptedException, UsernameHashNotAvailableException {
|
||||
final Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
|
||||
new ArrayList<>());
|
||||
for (int i = 1; i <= 9; i++) {
|
||||
ArrayList<byte[]> usernameHashes = new ArrayList<>(Arrays.asList(USERNAME_HASH_1, USERNAME_HASH_2));
|
||||
for (byte[] hash : usernameHashes) {
|
||||
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder()
|
||||
.tableName(USERNAMES_TABLE_NAME)
|
||||
.item(Map.of(
|
||||
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(UUID.randomUUID()),
|
||||
Accounts.ATTR_USERNAME, AttributeValues.fromString(usernameGenerator.fromParts("n00bkiller", i))))
|
||||
Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(hash)))
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
byte[] availableHash = new byte[32];
|
||||
new SecureRandom().nextBytes(availableHash);
|
||||
usernameHashes.add(availableHash);
|
||||
|
||||
// first time this is called lie and say the username is available
|
||||
// this simulates seeing an available username and then it being taken
|
||||
// by someone before the write
|
||||
doReturn(true).doCallRealMethod().when(accounts).usernameAvailable(any());
|
||||
final String username = accountsManager
|
||||
.setUsername(account, "n00bkiller", null)
|
||||
.getUsername().orElseThrow();
|
||||
assertThat(username).startsWith("n00bkiller");
|
||||
assertThat(discriminator(username)).isGreaterThanOrEqualTo(10).isLessThan(100);
|
||||
doReturn(true).doCallRealMethod().when(accounts).usernameHashAvailable(any());
|
||||
final byte[] username = accountsManager
|
||||
.reserveUsernameHash(account, usernameHashes)
|
||||
.reservedUsernameHash();
|
||||
|
||||
assertArrayEquals(username, availableHash);
|
||||
|
||||
// 1 attempt on first try (returns true),
|
||||
// 10 (attempts per width) on width=2 discriminators (all taken)
|
||||
verify(accounts, times(11)).usernameAvailable(argThat(un -> discriminator(un) < 10));
|
||||
|
||||
// 1 final attempt on width=3 discriminators
|
||||
verify(accounts, times(1)).usernameAvailable(argThat(un -> discriminator(un) >= 10));
|
||||
// 5 more attempts until "availableHash" returns true
|
||||
verify(accounts, times(4)).usernameHashAvailable(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReserveSetClear()
|
||||
throws InterruptedException, UsernameNotAvailableException, UsernameReservationNotFoundException {
|
||||
public void testReserveConfirmClear()
|
||||
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||
Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
|
||||
new ArrayList<>());
|
||||
AccountsManager.UsernameReservation reservation = accountsManager.reserveUsername(account, "n00bkiller");
|
||||
account = reservation.account();
|
||||
assertThat(account.getReservedUsernameHash()).isPresent();
|
||||
assertThat(reservation.reservedUsername()).startsWith("n00bkiller");
|
||||
int discriminator = discriminator(reservation.reservedUsername());
|
||||
assertThat(discriminator).isGreaterThan(0).isLessThan(10);
|
||||
assertThat(accountsManager.getByUsername(reservation.reservedUsername())).isEmpty();
|
||||
|
||||
account = accountsManager.confirmReservedUsername(
|
||||
account,
|
||||
reservation.reservedUsername(),
|
||||
reservation.reservationToken());
|
||||
// reserve
|
||||
AccountsManager.UsernameReservation reservation = accountsManager.reserveUsernameHash(account, List.of(
|
||||
USERNAME_HASH_1));
|
||||
assertArrayEquals(reservation.account().getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
assertThat(accountsManager.getByUsernameHash(reservation.reservedUsernameHash())).isEmpty();
|
||||
|
||||
assertThat(account.getUsername().get()).startsWith("n00bkiller");
|
||||
assertThat(accountsManager.getByUsername(account.getUsername().get()).orElseThrow().getUuid()).isEqualTo(
|
||||
// confirm
|
||||
account = accountsManager.confirmReservedUsernameHash(
|
||||
reservation.account(),
|
||||
reservation.reservedUsernameHash());
|
||||
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).orElseThrow().getUuid()).isEqualTo(
|
||||
account.getUuid());
|
||||
|
||||
// reroll
|
||||
reservation = accountsManager.reserveUsername(account, "n00bkiller");
|
||||
account = reservation.account();
|
||||
account = accountsManager.confirmReservedUsername(
|
||||
account,
|
||||
reservation.reservedUsername(),
|
||||
reservation.reservationToken());
|
||||
|
||||
final String newUsername = account.getUsername().orElseThrow();
|
||||
assertThat(discriminator(account.getUsername().orElseThrow())).isNotEqualTo(discriminator);
|
||||
|
||||
// clear
|
||||
account = accountsManager.clearUsername(account);
|
||||
assertThat(accountsManager.getByUsername(newUsername)).isEmpty();
|
||||
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsername()).isEmpty();
|
||||
|
||||
account = accountsManager.clearUsernameHash(account);
|
||||
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
|
||||
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReservationLapsed()
|
||||
throws InterruptedException, UsernameNotAvailableException, UsernameReservationNotFoundException {
|
||||
// use a username generator that can retry a lot
|
||||
buildAccountsManager(1, 1, 1000000);
|
||||
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||
|
||||
final Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
|
||||
new ArrayList<>());
|
||||
AccountsManager.UsernameReservation reservation1 = accountsManager.reserveUsername(account, "n00bkiller");
|
||||
final String reservedUsername = reservation1.reservedUsername();
|
||||
AccountsManager.UsernameReservation reservation1 = accountsManager.reserveUsernameHash(account, List.of(
|
||||
USERNAME_HASH_1));
|
||||
|
||||
long past = Instant.now().minus(Duration.ofMinutes(1)).getEpochSecond();
|
||||
// force expiration
|
||||
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().updateItem(UpdateItemRequest.builder()
|
||||
.tableName(USERNAMES_TABLE_NAME)
|
||||
.key(Map.of(Accounts.ATTR_USERNAME, AttributeValues.fromString(reservedUsername)))
|
||||
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1)))
|
||||
.updateExpression("SET #ttl = :ttl")
|
||||
.expressionAttributeNames(Map.of("#ttl", Accounts.ATTR_TTL))
|
||||
.expressionAttributeValues(Map.of(":ttl", AttributeValues.fromLong(past)))
|
||||
.build());
|
||||
|
||||
int discriminator = discriminator(reservedUsername);
|
||||
|
||||
// use up all names except the reserved one
|
||||
for (int i = 1; i <= 9; i++) {
|
||||
if (i == discriminator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder()
|
||||
.tableName(USERNAMES_TABLE_NAME)
|
||||
.item(Map.of(
|
||||
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(UUID.randomUUID()),
|
||||
Accounts.ATTR_USERNAME, AttributeValues.fromString(usernameGenerator.fromParts("n00bkiller", i))))
|
||||
.build());
|
||||
}
|
||||
|
||||
// a different account should be able to reserve it
|
||||
Account account2 = accountsManager.create("+18005552222", "password", null, new AccountAttributes(),
|
||||
new ArrayList<>());
|
||||
final AccountsManager.UsernameReservation reservation2 = accountsManager.reserveUsername(account2, "n00bkiller"
|
||||
);
|
||||
assertThat(reservation2.reservedUsername()).isEqualTo(reservedUsername);
|
||||
final AccountsManager.UsernameReservation reservation2 = accountsManager.reserveUsernameHash(account2, List.of(
|
||||
USERNAME_HASH_1));
|
||||
assertArrayEquals(reservation2.reservedUsernameHash(), USERNAME_HASH_1);
|
||||
|
||||
assertThrows(UsernameNotAvailableException.class,
|
||||
() -> accountsManager.confirmReservedUsername(reservation1.account(), reservedUsername, reservation1.reservationToken()));
|
||||
accountsManager.confirmReservedUsername(reservation2.account(), reservedUsername, reservation2.reservationToken());
|
||||
assertThrows(UsernameHashNotAvailableException.class,
|
||||
() -> accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1));
|
||||
account2 = accountsManager.confirmReservedUsernameHash(reservation2.account(), USERNAME_HASH_1);
|
||||
assertEquals(accountsManager.getByUsernameHash(USERNAME_HASH_1).orElseThrow().getUuid(), account2.getUuid());
|
||||
assertArrayEquals(account2.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUsernameReserveClearSetReserved()
|
||||
throws InterruptedException, UsernameNotAvailableException, UsernameReservationNotFoundException {
|
||||
void testUsernameSetReserveAnotherClearSetReserved()
|
||||
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||
Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
|
||||
new ArrayList<>());
|
||||
account = accountsManager.setUsername(account, "n00bkiller", null);
|
||||
final AccountsManager.UsernameReservation reservation = accountsManager.reserveUsername(account, "other");
|
||||
account = reservation.account();
|
||||
|
||||
assertThat(reservation.reservedUsername()).startsWith("other");
|
||||
assertThat(account.getUsername()).hasValueSatisfying(s -> s.startsWith("n00bkiller"));
|
||||
// Set username hash
|
||||
final AccountsManager.UsernameReservation reservation1 = accountsManager.reserveUsernameHash(account, List.of(
|
||||
USERNAME_HASH_1));
|
||||
account = accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1);
|
||||
|
||||
account = accountsManager.clearUsername(account);
|
||||
// Reserve another hash on the same account
|
||||
final AccountsManager.UsernameReservation reservation2 = accountsManager.reserveUsernameHash(account, List.of(
|
||||
USERNAME_HASH_2));
|
||||
account = reservation2.account();
|
||||
|
||||
assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_2);
|
||||
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
|
||||
// Clear the set username hash but not the reserved one
|
||||
account = accountsManager.clearUsernameHash(account);
|
||||
assertThat(account.getReservedUsernameHash()).isPresent();
|
||||
assertThat(account.getUsername()).isEmpty();
|
||||
assertThat(account.getUsernameHash()).isEmpty();
|
||||
|
||||
account = accountsManager.confirmReservedUsername(account, reservation.reservedUsername(), reservation.reservationToken());
|
||||
assertThat(account.getUsername()).hasValueSatisfying(s -> s.startsWith("other"));
|
||||
// Confirm second reservation
|
||||
account = accountsManager.confirmReservedUsernameHash(account, reservation2.reservedUsernameHash());
|
||||
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -21,6 +23,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -28,7 +31,6 @@ import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -66,9 +68,15 @@ class AccountsTest {
|
||||
private static final String NUMBER_CONSTRAINT_TABLE_NAME = "numbers_test";
|
||||
private static final String PNI_CONSTRAINT_TABLE_NAME = "pni_test";
|
||||
private static final String USERNAME_CONSTRAINT_TABLE_NAME = "username_test";
|
||||
private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
|
||||
private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
|
||||
private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1);
|
||||
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
|
||||
|
||||
private static final int SCAN_PAGE_SIZE = 1;
|
||||
|
||||
|
||||
|
||||
@RegisterExtension
|
||||
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
|
||||
.tableName(ACCOUNTS_TABLE_NAME)
|
||||
@@ -118,12 +126,12 @@ class AccountsTest {
|
||||
CreateTableRequest createUsernamesTableRequest = CreateTableRequest.builder()
|
||||
.tableName(USERNAME_CONSTRAINT_TABLE_NAME)
|
||||
.keySchema(KeySchemaElement.builder()
|
||||
.attributeName(Accounts.ATTR_USERNAME)
|
||||
.attributeName(Accounts.ATTR_USERNAME_HASH)
|
||||
.keyType(KeyType.HASH)
|
||||
.build())
|
||||
.attributeDefinitions(AttributeDefinition.builder()
|
||||
.attributeName(Accounts.ATTR_USERNAME)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.attributeName(Accounts.ATTR_USERNAME_HASH)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
|
||||
.build();
|
||||
@@ -614,40 +622,38 @@ class AccountsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsername() {
|
||||
void testSwitchUsernameHashes() {
|
||||
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account);
|
||||
|
||||
final String username = "TeST";
|
||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
|
||||
|
||||
assertThat(accounts.getByUsername(username)).isEmpty();
|
||||
|
||||
accounts.setUsername(account, username);
|
||||
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
accounts.confirmUsernameHash(account, USERNAME_HASH_1);
|
||||
|
||||
{
|
||||
final Optional<Account> maybeAccount = accounts.getByUsername(username);
|
||||
final Optional<Account> maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_1);
|
||||
|
||||
assertThat(maybeAccount).hasValueSatisfying(retrievedAccount ->
|
||||
assertThat(retrievedAccount.getUsername()).hasValueSatisfying(retrievedUsername ->
|
||||
assertThat(retrievedUsername).isEqualTo(username)));
|
||||
assertThat(retrievedAccount.getUsernameHash()).hasValueSatisfying(retrievedUsernameHash ->
|
||||
assertArrayEquals(retrievedUsernameHash, USERNAME_HASH_1)));
|
||||
|
||||
verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(), maybeAccount.orElseThrow(), account);
|
||||
}
|
||||
|
||||
final String secondUsername = username + "2";
|
||||
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1));
|
||||
accounts.confirmUsernameHash(account, USERNAME_HASH_2);
|
||||
|
||||
accounts.setUsername(account, secondUsername);
|
||||
|
||||
assertThat(accounts.getByUsername(username)).isEmpty();
|
||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
|
||||
assertThat(dynamoDbExtension.getDynamoDbClient()
|
||||
.getItem(GetItemRequest.builder()
|
||||
.tableName(USERNAME_CONSTRAINT_TABLE_NAME)
|
||||
.key(Map.of(Accounts.ATTR_USERNAME, AttributeValues.fromString("test")))
|
||||
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1)))
|
||||
.build())
|
||||
.item()).isEmpty();
|
||||
|
||||
{
|
||||
final Optional<Account> maybeAccount = accounts.getByUsername(secondUsername);
|
||||
final Optional<Account> maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_2);
|
||||
|
||||
assertThat(maybeAccount).isPresent();
|
||||
verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(),
|
||||
@@ -656,38 +662,51 @@ class AccountsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameConflict() {
|
||||
void testUsernameHashConflict() {
|
||||
final Account firstAccount = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
||||
final Account secondAccount = generateAccount("+18005559876", UUID.randomUUID(), UUID.randomUUID());
|
||||
|
||||
accounts.create(firstAccount);
|
||||
accounts.create(secondAccount);
|
||||
|
||||
final String username = "test";
|
||||
// first account reserves and confirms username hash
|
||||
assertThatNoException().isThrownBy(() -> {
|
||||
accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1);
|
||||
});
|
||||
|
||||
assertThatNoException().isThrownBy(() -> accounts.setUsername(firstAccount, username));
|
||||
|
||||
final Optional<Account> maybeAccount = accounts.getByUsername(username);
|
||||
final Optional<Account> maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_1);
|
||||
|
||||
assertThat(maybeAccount).isPresent();
|
||||
verifyStoredState(firstAccount.getNumber(), firstAccount.getUuid(), firstAccount.getPhoneNumberIdentifier(), maybeAccount.get(), firstAccount);
|
||||
|
||||
// throw an error if second account tries to reserve or confirm the same username hash
|
||||
assertThatExceptionOfType(ContestedOptimisticLockException.class)
|
||||
.isThrownBy(() -> accounts.setUsername(secondAccount, username));
|
||||
.isThrownBy(() -> accounts.reserveUsernameHash(secondAccount, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||
assertThatExceptionOfType(ContestedOptimisticLockException.class)
|
||||
.isThrownBy(() -> accounts.confirmUsernameHash(secondAccount, USERNAME_HASH_1));
|
||||
|
||||
assertThat(secondAccount.getUsername()).isEmpty();
|
||||
// throw an error if first account tries to reserve or confirm the username hash that it has already confirmed
|
||||
assertThatExceptionOfType(ContestedOptimisticLockException.class)
|
||||
.isThrownBy(() -> accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||
assertThatExceptionOfType(ContestedOptimisticLockException.class)
|
||||
.isThrownBy(() -> accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1));
|
||||
|
||||
assertThat(secondAccount.getReservedUsernameHash()).isEmpty();
|
||||
assertThat(secondAccount.getUsernameHash()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsernameVersionMismatch() {
|
||||
void testConfirmUsernameHashVersionMismatch() {
|
||||
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account);
|
||||
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
account.setVersion(account.getVersion() + 77);
|
||||
|
||||
assertThatExceptionOfType(ContestedOptimisticLockException.class)
|
||||
.isThrownBy(() -> accounts.setUsername(account, "test"));
|
||||
.isThrownBy(() -> accounts.confirmUsernameHash(account, USERNAME_HASH_1));
|
||||
|
||||
assertThat(account.getUsername()).isEmpty();
|
||||
assertThat(account.getUsernameHash()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -695,16 +714,15 @@ class AccountsTest {
|
||||
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account);
|
||||
|
||||
final String username = "TeST";
|
||||
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
accounts.confirmUsernameHash(account, USERNAME_HASH_1);
|
||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isPresent();
|
||||
|
||||
accounts.setUsername(account, username);
|
||||
assertThat(accounts.getByUsername(username)).isPresent();
|
||||
accounts.clearUsernameHash(account);
|
||||
|
||||
accounts.clearUsername(account);
|
||||
|
||||
assertThat(accounts.getByUsername(username)).isEmpty();
|
||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
|
||||
assertThat(accounts.getByAccountIdentifier(account.getUuid()))
|
||||
.hasValueSatisfying(clearedAccount -> assertThat(clearedAccount.getUsername()).isEmpty());
|
||||
.hasValueSatisfying(clearedAccount -> assertThat(clearedAccount.getUsernameHash()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -712,7 +730,7 @@ class AccountsTest {
|
||||
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account);
|
||||
|
||||
assertThatNoException().isThrownBy(() -> accounts.clearUsername(account));
|
||||
assertThatNoException().isThrownBy(() -> accounts.clearUsernameHash(account));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -720,167 +738,136 @@ class AccountsTest {
|
||||
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account);
|
||||
|
||||
final String username = "test";
|
||||
|
||||
accounts.setUsername(account, username);
|
||||
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
accounts.confirmUsernameHash(account, USERNAME_HASH_1);
|
||||
|
||||
account.setVersion(account.getVersion() + 12);
|
||||
|
||||
assertThatExceptionOfType(ContestedOptimisticLockException.class).isThrownBy(() -> accounts.clearUsername(account));
|
||||
assertThatExceptionOfType(ContestedOptimisticLockException.class).isThrownBy(() -> accounts.clearUsernameHash(account));
|
||||
|
||||
assertThat(account.getUsername()).hasValueSatisfying(u -> assertThat(u).isEqualTo(username));
|
||||
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReservedUsername() {
|
||||
void testReservedUsernameHash() {
|
||||
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account1);
|
||||
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account2);
|
||||
|
||||
final UUID token = accounts.reserveUsername(account1, "GarfielD", Duration.ofDays(1));
|
||||
assertThat(account1.getReservedUsernameHash()).get().isEqualTo(Accounts.reservedUsernameHash(account1.getUuid(), "GarfielD"));
|
||||
assertThat(account1.getUsername()).isEmpty();
|
||||
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
assertArrayEquals(account1.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
assertThat(account1.getUsernameHash()).isEmpty();
|
||||
|
||||
// account 2 shouldn't be able to reserve the username if it's the same when normalized
|
||||
// account 2 shouldn't be able to reserve or confirm the same username hash
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.reserveUsername(account2, "gARFIELd", Duration.ofDays(1)));
|
||||
() -> accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.confirmUsername(account2, "gARFIELd", UUID.randomUUID()));
|
||||
assertThat(accounts.getByUsername("gARFIELd")).isEmpty();
|
||||
() -> accounts.confirmUsernameHash(account2, USERNAME_HASH_1));
|
||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
|
||||
|
||||
accounts.confirmUsername(account1, "GarfielD", token);
|
||||
accounts.confirmUsernameHash(account1, USERNAME_HASH_1);
|
||||
assertThat(account1.getReservedUsernameHash()).isEmpty();
|
||||
assertThat(account1.getUsername()).get().isEqualTo("GarfielD");
|
||||
assertThat(accounts.getByUsername("GarfielD").get().getUuid()).isEqualTo(account1.getUuid());
|
||||
assertArrayEquals(account1.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).get().getUuid()).isEqualTo(account1.getUuid());
|
||||
|
||||
final Map<String, AttributeValue> usernameConstraintRecord = dynamoDbExtension.getDynamoDbClient()
|
||||
.getItem(GetItemRequest.builder()
|
||||
.tableName(USERNAME_CONSTRAINT_TABLE_NAME)
|
||||
.key(Map.of(Accounts.ATTR_USERNAME, AttributeValues.fromString("garfield")))
|
||||
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1)))
|
||||
.build())
|
||||
.item();
|
||||
|
||||
assertThat(usernameConstraintRecord).containsKey(Accounts.ATTR_USERNAME);
|
||||
assertThat(usernameConstraintRecord).containsKey(Accounts.ATTR_USERNAME_HASH);
|
||||
assertThat(usernameConstraintRecord).doesNotContainKey(Accounts.ATTR_TTL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUsernameAvailable() {
|
||||
void testUsernameHashAvailable() {
|
||||
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account1);
|
||||
|
||||
final String username = "UnSinkaBlesam";
|
||||
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1)).isFalse();
|
||||
assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1)).isFalse();
|
||||
assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1)).isFalse();
|
||||
assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1)).isTrue();
|
||||
|
||||
final UUID token = accounts.reserveUsername(account1, username, Duration.ofDays(1));
|
||||
assertThat(accounts.usernameAvailable(username)).isFalse();
|
||||
assertThat(accounts.usernameAvailable(Optional.empty(), username)).isFalse();
|
||||
assertThat(accounts.usernameAvailable(Optional.of(UUID.randomUUID()), username)).isFalse();
|
||||
assertThat(accounts.usernameAvailable(Optional.of(token), username)).isTrue();
|
||||
|
||||
accounts.confirmUsername(account1, username, token);
|
||||
assertThat(accounts.usernameAvailable(username)).isFalse();
|
||||
assertThat(accounts.usernameAvailable(Optional.empty(), username)).isFalse();
|
||||
assertThat(accounts.usernameAvailable(Optional.of(UUID.randomUUID()), username)).isFalse();
|
||||
assertThat(accounts.usernameAvailable(Optional.of(token), username)).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testReservedUsernameWrongToken() {
|
||||
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account);
|
||||
accounts.reserveUsername(account, "grumpy", Duration.ofDays(1));
|
||||
assertThat(account.getReservedUsernameHash())
|
||||
.get()
|
||||
.isEqualTo(Accounts.reservedUsernameHash(account.getUuid(), "grumpy"));
|
||||
assertThat(account.getUsername()).isEmpty();
|
||||
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.confirmUsername(account, "grumpy", UUID.randomUUID()));
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.setUsername(account, "grumpy"));
|
||||
accounts.confirmUsernameHash(account1, USERNAME_HASH_1);
|
||||
assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1)).isFalse();
|
||||
assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1)).isFalse();
|
||||
assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1)).isFalse();
|
||||
assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReserveExpiredReservedUsername() {
|
||||
void testConfirmReservedUsernameHashWrongAccountUuid() {
|
||||
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account1);
|
||||
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account2);
|
||||
final String username = "snowball.02";
|
||||
|
||||
accounts.reserveUsername(account1, username, Duration.ofDays(2));
|
||||
|
||||
Supplier<UUID> take = () -> accounts.reserveUsername(account2, username, Duration.ofDays(2));
|
||||
|
||||
for (int i = 0; i <= 2; i++) {
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(i)));
|
||||
assertThrows(ContestedOptimisticLockException.class, take::get);
|
||||
}
|
||||
|
||||
// after 2 days, can take the name
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1)));
|
||||
final UUID token = take.get();
|
||||
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
assertArrayEquals(account1.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
assertThat(account1.getUsernameHash()).isEmpty();
|
||||
|
||||
// only account1 should be able to confirm the reserved hash
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.reserveUsername(account1, username, Duration.ofDays(2)));
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.setUsername(account1, username));
|
||||
|
||||
accounts.confirmUsername(account2, username, token);
|
||||
assertThat(accounts.getByUsername(username).get().getUuid()).isEqualTo(account2.getUuid());
|
||||
() -> accounts.confirmUsernameHash(account2, USERNAME_HASH_1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTakeExpiredReservedUsername() {
|
||||
void testConfirmExpiredReservedUsernameHash() {
|
||||
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account1);
|
||||
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account2);
|
||||
final String username = "simon.123";
|
||||
|
||||
accounts.reserveUsername(account1, username, Duration.ofDays(2));
|
||||
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2));
|
||||
|
||||
Runnable take = () -> accounts.setUsername(account2, username);
|
||||
Runnable runnable = () -> accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1));
|
||||
|
||||
for (int i = 0; i <= 2; i++) {
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(i)));
|
||||
assertThrows(ContestedOptimisticLockException.class, take::run);
|
||||
assertThrows(ContestedOptimisticLockException.class, runnable::run);
|
||||
}
|
||||
|
||||
// after 2 days, can take the name
|
||||
// after 2 days, can reserve and confirm the hash
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1)));
|
||||
take.run();
|
||||
runnable.run();
|
||||
assertEquals(account2.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||
|
||||
accounts.confirmUsernameHash(account2, USERNAME_HASH_1);
|
||||
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.reserveUsername(account1, username, Duration.ofDays(2)));
|
||||
() -> accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)));
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.setUsername(account1, username));
|
||||
assertThat(accounts.getByUsername(username).get().getUuid()).isEqualTo(account2.getUuid());
|
||||
() -> accounts.confirmUsernameHash(account1, USERNAME_HASH_1));
|
||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).get().getUuid()).isEqualTo(account2.getUuid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetryReserveUsername() {
|
||||
void testRetryReserveUsernameHash() {
|
||||
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account);
|
||||
accounts.reserveUsername(account, "jorts", Duration.ofDays(2));
|
||||
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(2));
|
||||
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.reserveUsername(account, "jorts", Duration.ofDays(2)),
|
||||
"Shouldn't be able to re-reserve same username (would extend ttl)");
|
||||
() -> accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(2)),
|
||||
"Shouldn't be able to re-reserve same username hash (would extend ttl)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReserveUsernameVersionConflict() {
|
||||
void testReserveConfirmUsernameHashVersionConflict() {
|
||||
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
||||
accounts.create(account);
|
||||
account.setVersion(account.getVersion() + 12);
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.reserveUsername(account, "salem", Duration.ofDays(1)));
|
||||
() -> accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||
assertThrows(ContestedOptimisticLockException.class,
|
||||
() -> accounts.setUsername(account, "salem"));
|
||||
|
||||
() -> accounts.confirmUsernameHash(account, USERNAME_HASH_1));
|
||||
assertThat(account.getReservedUsernameHash()).isEmpty();
|
||||
assertThat(account.getUsernameHash()).isEmpty();
|
||||
}
|
||||
|
||||
private Device generateDevice(long id) {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
|
||||
|
||||
class ProhibitedUsernamesTest {
|
||||
|
||||
private static final String RESERVED_USERNAMES_TABLE_NAME = "reserved_usernames_test";
|
||||
|
||||
@RegisterExtension
|
||||
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
|
||||
.tableName(RESERVED_USERNAMES_TABLE_NAME)
|
||||
.hashKey(ProhibitedUsernames.KEY_PATTERN)
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(ProhibitedUsernames.KEY_PATTERN)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
private static final UUID RESERVED_FOR_UUID = UUID.randomUUID();
|
||||
|
||||
private ProhibitedUsernames prohibitedUsernames;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
prohibitedUsernames =
|
||||
new ProhibitedUsernames(dynamoDbExtension.getDynamoDbClient(), RESERVED_USERNAMES_TABLE_NAME);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void isReserved(final String username, final UUID uuid, final boolean expectReserved) {
|
||||
prohibitedUsernames.prohibitUsername(".*myusername.*", RESERVED_FOR_UUID);
|
||||
prohibitedUsernames.prohibitUsername("^foobar$", RESERVED_FOR_UUID);
|
||||
|
||||
assertEquals(expectReserved, prohibitedUsernames.isProhibited(username, uuid));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> isReserved() {
|
||||
return Stream.of(
|
||||
Arguments.of("myusername", UUID.randomUUID(), true),
|
||||
Arguments.of("myusername", RESERVED_FOR_UUID, false),
|
||||
Arguments.of("thyusername", UUID.randomUUID(), false),
|
||||
Arguments.of("somemyusername", UUID.randomUUID(), true),
|
||||
Arguments.of("myusernamesome", UUID.randomUUID(), true),
|
||||
Arguments.of("somemyusernamesome", UUID.randomUUID(), true),
|
||||
Arguments.of("MYUSERNAME", UUID.randomUUID(), true),
|
||||
Arguments.of("foobar", UUID.randomUUID(), true),
|
||||
Arguments.of("foobar", RESERVED_FOR_UUID, false),
|
||||
Arguments.of("somefoobar", UUID.randomUUID(), false),
|
||||
Arguments.of("foobarsome", UUID.randomUUID(), false),
|
||||
Arguments.of("somefoobarsome", UUID.randomUUID(), false),
|
||||
Arguments.of("FOOBAR", UUID.randomUUID(), true));
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ public class AccountsHelper {
|
||||
case "getUuid" -> when(updatedAccount.getUuid()).thenAnswer(stubbing);
|
||||
case "getPhoneNumberIdentifier" -> when(updatedAccount.getPhoneNumberIdentifier()).thenAnswer(stubbing);
|
||||
case "getNumber" -> when(updatedAccount.getNumber()).thenAnswer(stubbing);
|
||||
case "getUsername" -> when(updatedAccount.getUsername()).thenAnswer(stubbing);
|
||||
case "getUsername" -> when(updatedAccount.getUsernameHash()).thenAnswer(stubbing);
|
||||
case "getDevices" -> when(updatedAccount.getDevices()).thenAnswer(stubbing);
|
||||
case "getDevice" -> when(updatedAccount.getDevice(stubbing.getInvocation().getArgument(0))).thenAnswer(stubbing);
|
||||
case "getMasterDevice" -> when(updatedAccount.getMasterDevice()).thenAnswer(stubbing);
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.tests.util;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
|
||||
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class UsernameGeneratorTest {
|
||||
|
||||
private static final Duration TTL = Duration.ofMinutes(5);
|
||||
|
||||
@ParameterizedTest(name = "[{index}]:{0} ({2})")
|
||||
@MethodSource
|
||||
public void nicknameValidation(String nickname, boolean valid, String testCaseName) {
|
||||
assertThat(UsernameGenerator.isValidNickname(nickname)).isEqualTo(valid);
|
||||
}
|
||||
|
||||
static Stream<Arguments> nicknameValidation() {
|
||||
return Stream.of(
|
||||
Arguments.of("Test", true, "upper case"),
|
||||
Arguments.of("tesT", true, "upper case"),
|
||||
Arguments.of("te-st", false, "illegal character"),
|
||||
Arguments.of("ab\uD83D\uDC1B", false, "illegal character"),
|
||||
Arguments.of("1test", false, "illegal start"),
|
||||
Arguments.of("test#123", false, "illegal character"),
|
||||
Arguments.of("test.123", false, "illegal character"),
|
||||
Arguments.of("ab", false, "too short"),
|
||||
Arguments.of("", false, ""),
|
||||
Arguments.of("_123456789_123456789_123456789123", false, "33 characters"),
|
||||
|
||||
Arguments.of("_test", true, ""),
|
||||
Arguments.of("test", true, ""),
|
||||
Arguments.of("test123", true, ""),
|
||||
Arguments.of("abc", true, ""),
|
||||
Arguments.of("_123456789_123456789_12345678912", true, "32 characters")
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name="[{index}]: {0}")
|
||||
@MethodSource
|
||||
public void nonStandardUsernames(final String username, final boolean isStandard) {
|
||||
assertThat(UsernameGenerator.isStandardFormat(username)).isEqualTo(isStandard);
|
||||
}
|
||||
|
||||
static Stream<Arguments> nonStandardUsernames() {
|
||||
return Stream.of(
|
||||
Arguments.of("Test.123", true),
|
||||
Arguments.of("test.-123", false),
|
||||
Arguments.of("test.0", false),
|
||||
Arguments.of("test.", false),
|
||||
Arguments.of("test.1_00", false),
|
||||
|
||||
Arguments.of("test.1", true),
|
||||
Arguments.of("abc.1234", true)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroPadDiscriminators() {
|
||||
final UsernameGenerator generator = new UsernameGenerator(4, 5, 1, TTL);
|
||||
assertThat(generator.fromParts("test", 1)).isEqualTo("test.0001");
|
||||
assertThat(generator.fromParts("test", 123)).isEqualTo("test.0123");
|
||||
assertThat(generator.fromParts("test", 9999)).isEqualTo("test.9999");
|
||||
assertThat(generator.fromParts("test", 99999)).isEqualTo("test.99999");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expectedWidth() throws UsernameNotAvailableException {
|
||||
String username = new UsernameGenerator(1, 6, 1, TTL).generateAvailableUsername("test", t -> true);
|
||||
assertThat(extractDiscriminator(username)).isGreaterThan(0).isLessThan(10);
|
||||
|
||||
username = new UsernameGenerator(2, 6, 1, TTL).generateAvailableUsername("test", t -> true);
|
||||
assertThat(extractDiscriminator(username)).isGreaterThan(0).isLessThan(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandDiscriminator() throws UsernameNotAvailableException {
|
||||
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
|
||||
final String username = ug.generateAvailableUsername("test", allowDiscriminator(d -> d >= 10000));
|
||||
int discriminator = extractDiscriminator(username);
|
||||
assertThat(discriminator).isGreaterThanOrEqualTo(10000).isLessThan(100000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandDiscriminatorToMax() throws UsernameNotAvailableException {
|
||||
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
|
||||
final String username = ug.generateAvailableUsername("test", allowDiscriminator(d -> d >= 100000));
|
||||
int discriminator = extractDiscriminator(username);
|
||||
assertThat(discriminator).isGreaterThanOrEqualTo(100000).isLessThan(1000000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustDiscriminator() {
|
||||
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
|
||||
Assertions.assertThrows(UsernameNotAvailableException.class, () -> {
|
||||
// allow greater than our max width
|
||||
ug.generateAvailableUsername("test", allowDiscriminator(d -> d >= 1000000));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomCoverageMinWidth() throws UsernameNotAvailableException {
|
||||
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
|
||||
final Set<Integer> seen = new HashSet<>();
|
||||
for (int i = 0; i < 1000 && seen.size() < 9; i++) {
|
||||
seen.add(extractDiscriminator(ug.generateAvailableUsername("test", ignored -> true)));
|
||||
}
|
||||
// after 1K iterations, probability of a missed value is (9/10)^999
|
||||
assertThat(seen.size()).isEqualTo(9);
|
||||
assertThat(seen).allMatch(i -> i > 0 && i < 10);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomCoverageMidWidth() throws UsernameNotAvailableException {
|
||||
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
|
||||
final Set<Integer> seen = new HashSet<>();
|
||||
for (int i = 0; i < 100000 && seen.size() < 90; i++) {
|
||||
seen.add(extractDiscriminator(ug.generateAvailableUsername("test", allowDiscriminator(d -> d >= 10))));
|
||||
}
|
||||
// after 100K iterations, probability of a missed value is (99/100)^99999
|
||||
assertThat(seen.size()).isEqualTo(90);
|
||||
assertThat(seen).allMatch(i -> i >= 10 && i < 100);
|
||||
|
||||
}
|
||||
|
||||
private static Predicate<String> allowDiscriminator(Predicate<Integer> p) {
|
||||
return username -> p.test(extractDiscriminator(username));
|
||||
}
|
||||
|
||||
private static int extractDiscriminator(final String username) {
|
||||
return Integer.parseInt(username.substring(username.indexOf(UsernameGenerator.SEPARATOR) + 1));
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.tests.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.util.UsernameNormalizer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class UsernameNormalizerTest {
|
||||
|
||||
@Test
|
||||
public void usernameNormalization() {
|
||||
assertThat(UsernameNormalizer.normalize("TeST")).isEqualTo("test");
|
||||
assertThat(UsernameNormalizer.normalize("TeST_")).isEqualTo("test_");
|
||||
assertThat(UsernameNormalizer.normalize("TeST_.123")).isEqualTo("test_.123");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
class NicknameValidatorTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void isValid(final String username, final boolean expectValid) {
|
||||
final NicknameValidator nicknameValidator = new NicknameValidator();
|
||||
|
||||
assertEquals(expectValid, nicknameValidator.isValid(username, null));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> isValid() {
|
||||
return Stream.of(
|
||||
Arguments.of("test", true),
|
||||
Arguments.of("_test", true),
|
||||
Arguments.of("test123", true),
|
||||
Arguments.of("a", false), // Too short
|
||||
Arguments.of("thisisareallyreallyreallylongusernamethatwewouldnotalllow", false),
|
||||
Arguments.of("illegal character", false),
|
||||
Arguments.of("0test", false), // Illegal first character
|
||||
Arguments.of("pаypal", false), // Unicode confusable characters
|
||||
Arguments.of("test\uD83D\uDC4E", false), // Emoji
|
||||
Arguments.of(" ", false),
|
||||
Arguments.of("", false),
|
||||
Arguments.of(null, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user