Support for versioned profiles

Includes support for issuing zkgroup auth credentials
This commit is contained in:
Moxie Marlinspike
2019-10-09 11:30:01 -07:00
parent a94fc22659
commit ba3102d667
23 changed files with 1315 additions and 98 deletions

View File

@@ -4,18 +4,27 @@ import com.google.common.collect.ImmutableSet;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.ClassRule;
import org.junit.Test;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.ServerSecretParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.auth.AuthCredential;
import org.signal.zkgroup.auth.AuthCredentialResponse;
import org.signal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.zkgroup.auth.ServerZkAuthOperations;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.crypto.Curve;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util;
import javax.ws.rs.core.Response;
import java.io.IOException;
@@ -24,6 +33,7 @@ import java.util.Arrays;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static junit.framework.TestCase.assertTrue;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -35,11 +45,14 @@ public class CertificateControllerTest {
private static final String signingCertificate = "CiUIDBIhBbTz4h1My+tt+vw+TVscgUe/DeHS0W02tPWAWbTO2xc3EkD+go4bJnU0AcnFfbOLKoiBfCzouZtDYMOVi69rE7r4U9cXREEqOkUmU2WJBjykAxWPCcSTmVTYHDw7hkSp/puG";
private static final String signingKey = "ABOxG29xrfq4E7IrW11Eg7+HBbtba9iiS0500YoBjn4=";
private static CertificateGenerator certificateGenerator;
private static ServerSecretParams serverSecretParams = ServerSecretParams.generate();
private static CertificateGenerator certificateGenerator;
private static ServerZkAuthOperations serverZkAuthOperations;
static {
try {
certificateGenerator = new CertificateGenerator(Base64.decode(signingCertificate), Curve.decodePrivatePoint(Base64.decode(signingKey)), 1);
certificateGenerator = new CertificateGenerator(Base64.decode(signingCertificate), Curve.decodePrivatePoint(Base64.decode(signingKey)), 1);
serverZkAuthOperations = new ServerZkAuthOperations(serverSecretParams);
} catch (IOException e) {
throw new AssertionError(e);
}
@@ -52,7 +65,7 @@ public class CertificateControllerTest {
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new CertificateController(certificateGenerator))
.addResource(new CertificateController(certificateGenerator, serverZkAuthOperations))
.build();
@@ -150,5 +163,73 @@ public class CertificateControllerTest {
assertEquals(response.getStatus(), 401);
}
@Test
public void testGetSingleAuthCredential() throws InvalidInputException, VerificationFailedException {
GroupCredentials credentials = resources.getJerseyTest()
.target("/v1/certificate/group/" + Util.currentDaysSinceEpoch() + "/" + Util.currentDaysSinceEpoch())
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(GroupCredentials.class);
assertThat(credentials.getCredentials().size()).isEqualTo(1);
assertThat(credentials.getCredentials().get(0).getRedemptionTime()).isEqualTo(Util.currentDaysSinceEpoch());
ClientZkAuthOperations clientZkAuthOperations = new ClientZkAuthOperations(serverSecretParams.getPublicParams());
AuthCredential credential = clientZkAuthOperations.receiveAuthCredential(AuthHelper.VALID_UUID, Util.currentDaysSinceEpoch(), new AuthCredentialResponse(credentials.getCredentials().get(0).getCredential()));
}
@Test
public void testGetWeekLongAuthCredentials() throws InvalidInputException, VerificationFailedException {
GroupCredentials credentials = resources.getJerseyTest()
.target("/v1/certificate/group/" + Util.currentDaysSinceEpoch() + "/" + (Util.currentDaysSinceEpoch() + 7))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(GroupCredentials.class);
assertThat(credentials.getCredentials().size()).isEqualTo(8);
for (int i=0;i<=7;i++) {
assertThat(credentials.getCredentials().get(i).getRedemptionTime()).isEqualTo(Util.currentDaysSinceEpoch() + i);
ClientZkAuthOperations clientZkAuthOperations = new ClientZkAuthOperations(serverSecretParams.getPublicParams());
AuthCredential credential = clientZkAuthOperations.receiveAuthCredential(AuthHelper.VALID_UUID, Util.currentDaysSinceEpoch() + i , new AuthCredentialResponse(credentials.getCredentials().get(i).getCredential()));
}
}
@Test
public void testTooManyDaysOut() throws InvalidInputException {
Response response = resources.getJerseyTest()
.target("/v1/certificate/group/" + Util.currentDaysSinceEpoch() + "/" + (Util.currentDaysSinceEpoch() + 8))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void testBackwardsInTime() throws InvalidInputException {
Response response = resources.getJerseyTest()
.target("/v1/certificate/group/" + (Util.currentDaysSinceEpoch() - 1) + "/" + (Util.currentDaysSinceEpoch() + 7))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void testBadAuth() throws InvalidInputException {
Response response = resources.getJerseyTest()
.target("/v1/certificate/group/" + Util.currentDaysSinceEpoch() + "/" + (Util.currentDaysSinceEpoch() + 7))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(401);
}
}

View File

@@ -1,5 +1,7 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.google.common.collect.ImmutableSet;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
@@ -8,21 +10,32 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Optional;
@@ -34,18 +47,17 @@ import static org.mockito.Mockito.*;
public class ProfileControllerTest {
private static AccountsManager accountsManager = mock(AccountsManager.class );
private static ProfilesManager profilesManager = mock(ProfilesManager.class);
private static UsernamesManager usernamesManager = mock(UsernamesManager.class);
private static RateLimiters rateLimiters = mock(RateLimiters.class );
private static RateLimiter rateLimiter = mock(RateLimiter.class );
private static RateLimiter usernameRateLimiter = mock(RateLimiter.class );
private static CdnConfiguration configuration = mock(CdnConfiguration.class);
static {
when(configuration.getAccessKey()).thenReturn("accessKey");
when(configuration.getAccessSecret()).thenReturn("accessSecret");
when(configuration.getRegion()).thenReturn("us-east-1");
when(configuration.getBucket()).thenReturn("profile-bucket");
}
private static AmazonS3 s3client = mock(AmazonS3.class);
private static PostPolicyGenerator postPolicyGenerator = new PostPolicyGenerator("us-west-1", "profile-bucket", "accessKey");
private static PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1");
private static ServerZkProfileOperations zkProfileOperations = mock(ServerZkProfileOperations.class);
@ClassRule
public static final ResourceTestRule resources = ResourceTestRule.builder()
@@ -55,8 +67,13 @@ public class ProfileControllerTest {
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new ProfileController(rateLimiters,
accountsManager,
profilesManager,
usernamesManager,
configuration))
s3client,
postPolicyGenerator,
policySigner,
"profilesBucket",
zkProfileOperations))
.build();
@Before
@@ -93,7 +110,14 @@ public class ProfileControllerTest {
when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount));
when(accountsManager.get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER)))).thenReturn(Optional.of(capabilitiesAccount));
Mockito.clearInvocations(accountsManager);
when(profilesManager.get(eq(AuthHelper.VALID_UUID), eq("someversion"))).thenReturn(Optional.empty());
when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"))).thenReturn(Optional.of(new VersionedProfile("validversion", "validname", "profiles/validavatar", "validcommitmnet".getBytes())));
clearInvocations(rateLimiter);
clearInvocations(accountsManager);
clearInvocations(usernamesManager);
clearInvocations(usernameRateLimiter);
clearInvocations(profilesManager);
}
@Test
@@ -111,8 +135,7 @@ public class ProfileControllerTest {
verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasUuid() && identifier.getUuid().equals(AuthHelper.VALID_UUID_TWO)));
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(rateLimiter, times(2)).validate(eq(AuthHelper.VALID_NUMBER));
reset(rateLimiter);
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER));
}
@Test
@@ -133,7 +156,6 @@ public class ProfileControllerTest {
verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER_TWO)));
verifyNoMoreInteractions(usernamesManager);
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER));
reset(rateLimiter);
}
@Test
@@ -188,7 +210,6 @@ public class ProfileControllerTest {
verify(usernamesManager, times(1)).get(eq("n00bkillerzzzzz"));
verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID.toString()));
reset(usernameRateLimiter);
}
@@ -215,7 +236,7 @@ public class ProfileControllerTest {
}
@Test
public void testSetProfileName() {
public void testSetProfileNameDeprecated() {
Response response = resources.getJerseyTest()
.target("/v1/profile/name/123456789012345678901234567890123456789012345678901234567890123456789012")
.request()
@@ -228,7 +249,7 @@ public class ProfileControllerTest {
}
@Test
public void testSetProfileNameExtended() {
public void testSetProfileNameExtendedDeprecated() {
Response response = resources.getJerseyTest()
.target("/v1/profile/name/123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678")
.request()
@@ -241,7 +262,7 @@ public class ProfileControllerTest {
}
@Test
public void testSetProfileNameWrongSize() {
public void testSetProfileNameWrongSizeDeprecated() {
Response response = resources.getJerseyTest()
.target("/v1/profile/name/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")
.request()
@@ -252,4 +273,113 @@ public class ProfileControllerTest {
verifyNoMoreInteractions(accountsManager);
}
/////
@Test
public void testSetProfileWantAvatarUpload() throws InvalidInputException {
ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment();
ProfileAvatarUploadAttributes uploadAttributes = resources.getJerseyTest()
.target("/v1/profile/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new CreateProfileRequest(commitment, "someversion", "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", true), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class);
ArgumentCaptor<VersionedProfile> profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class);
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID), eq("someversion"));
verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID), profileArgumentCaptor.capture());
verifyNoMoreInteractions(s3client);
assertThat(profileArgumentCaptor.getValue().getCommitment()).isEqualTo(commitment.serialize());
assertThat(profileArgumentCaptor.getValue().getAvatar()).isEqualTo(uploadAttributes.getKey());
assertThat(profileArgumentCaptor.getValue().getVersion()).isEqualTo("someversion");
assertThat(profileArgumentCaptor.getValue().getName()).isEqualTo("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678");
}
@Test
public void testSetProfileWantAvatarUploadWithBadProfileSize() throws InvalidInputException {
ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment();
Response response = resources.getJerseyTest()
.target("/v1/profile/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new CreateProfileRequest(commitment, "someversion", "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", true), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(422);
}
@Test
public void testSetProfileWithoutAvatarUpload() throws InvalidInputException {
ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment();
Response response = resources.getJerseyTest()
.target("/v1/profile/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
.put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", false), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.hasEntity()).isFalse();
ArgumentCaptor<VersionedProfile> profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class);
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("anotherversion"));
verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture());
verifyNoMoreInteractions(s3client);
assertThat(profileArgumentCaptor.getValue().getCommitment()).isEqualTo(commitment.serialize());
assertThat(profileArgumentCaptor.getValue().getAvatar()).isNull();
assertThat(profileArgumentCaptor.getValue().getVersion()).isEqualTo("anotherversion");
assertThat(profileArgumentCaptor.getValue().getName()).isEqualTo("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678");
}
@Test
public void testSetProvfileWithAvatarUploadAndPreviousAvatar() throws InvalidInputException {
ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment();
ProfileAvatarUploadAttributes uploadAttributes= resources.getJerseyTest()
.target("/v1/profile/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
.put(Entity.entity(new CreateProfileRequest(commitment, "validversion", "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", true), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class);
ArgumentCaptor<VersionedProfile> profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class);
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"));
verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture());
verify(s3client, times(1)).deleteObject(eq("profilesBucket"), eq("profiles/validavatar"));
assertThat(profileArgumentCaptor.getValue().getCommitment()).isEqualTo(commitment.serialize());
assertThat(profileArgumentCaptor.getValue().getAvatar()).startsWith("profiles/");
assertThat(profileArgumentCaptor.getValue().getVersion()).isEqualTo("validversion");
assertThat(profileArgumentCaptor.getValue().getName()).isEqualTo("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678");
}
@Test
public void testGetProfileByVersion() throws RateLimitExceededException {
Profile profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
assertThat(profile.getIdentityKey()).isEqualTo("bar");
assertThat(profile.getName()).isEqualTo("validname");
assertThat(profile.getAvatar()).isEqualTo("profiles/validavatar");
assertThat(profile.getCapabilities().isUuid()).isFalse();
assertThat(profile.getUsername()).isEqualTo("n00bkiller");
assertThat(profile.getUuid()).isNull();;
verify(accountsManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"));
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER));
}
}

View File

@@ -0,0 +1,107 @@
package org.whispersystems.textsecuregcm.tests.storage;
import org.junit.Test;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.util.Base64;
import java.util.Optional;
import java.util.UUID;
import static junit.framework.TestCase.assertSame;
import static junit.framework.TestCase.assertTrue;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisException;
public class ProfilesManagerTest {
@Test
public void testGetProfileInCache() {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Profiles profiles = mock(Profiles.class );
UUID uuid = UUID.randomUUID();
when(cacheClient.getReadResource()).thenReturn(jedis);
when(jedis.hget(eq("profiles::" + uuid.toString()), eq("someversion"))).thenReturn("{\"version\": \"someversion\", \"name\": \"somename\", \"avatar\": \"someavatar\", \"commitment\":\"" + Base64.encodeBytes("somecommitment".getBytes()) + "\"}");
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
Optional<VersionedProfile> profile = profilesManager.get(uuid, "someversion");
assertTrue(profile.isPresent());
assertEquals(profile.get().getName(), "somename");
assertEquals(profile.get().getAvatar(), "someavatar");
assertThat(profile.get().getCommitment()).isEqualTo("somecommitment".getBytes());
verify(jedis, times(1)).hget(eq("profiles::" + uuid.toString()), eq("someversion"));
verify(jedis, times(1)).close();
verifyNoMoreInteractions(jedis);
verifyNoMoreInteractions(profiles);
}
@Test
public void testGetProfileNotInCache() {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Profiles profiles = mock(Profiles.class );
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("someversion", "somename", "someavatar", "somecommitment".getBytes());
when(cacheClient.getReadResource()).thenReturn(jedis);
when(cacheClient.getWriteResource()).thenReturn(jedis);
when(jedis.hget(eq("profiles::" + uuid.toString()), eq("someversion"))).thenReturn(null);
when(profiles.get(eq(uuid), eq("someversion"))).thenReturn(Optional.of(profile));
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
Optional<VersionedProfile> retrieved = profilesManager.get(uuid, "someversion");
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), profile);
verify(jedis, times(1)).hget(eq("profiles::" + uuid.toString()), eq("someversion"));
verify(jedis, times(1)).hset(eq("profiles::" + uuid.toString()), eq("someversion"), anyString());
verify(jedis, times(2)).close();
verifyNoMoreInteractions(jedis);
verify(profiles, times(1)).get(eq(uuid), eq("someversion"));
verifyNoMoreInteractions(profiles);
}
@Test
public void testGetProfileBrokenCache() {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Profiles profiles = mock(Profiles.class );
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("someversion", "somename", "someavatar", "somecommitment".getBytes());
when(cacheClient.getReadResource()).thenReturn(jedis);
when(cacheClient.getWriteResource()).thenReturn(jedis);
when(jedis.hget(eq("profiles::" + uuid.toString()), eq("someversion"))).thenThrow(new JedisException("Connection lost"));
when(profiles.get(eq(uuid), eq("someversion"))).thenReturn(Optional.of(profile));
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
Optional<VersionedProfile> retrieved = profilesManager.get(uuid, "someversion");
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), profile);
verify(jedis, times(1)).hget(eq("profiles::" + uuid.toString()), eq("someversion"));
verify(jedis, times(1)).hset(eq("profiles::" + uuid.toString()), eq("someversion"), anyString());
verify(jedis, times(2)).close();
verifyNoMoreInteractions(jedis);
verify(profiles, times(1)).get(eq(uuid), eq("someversion"));
verifyNoMoreInteractions(profiles);
}
}

View File

@@ -0,0 +1,130 @@
package org.whispersystems.textsecuregcm.tests.storage;
import com.opentable.db.postgres.embedded.LiquibasePreparer;
import com.opentable.db.postgres.junit.EmbeddedPostgresRules;
import com.opentable.db.postgres.junit.PreparedDbRule;
import org.jdbi.v3.core.Jdbi;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class ProfilesTest {
@Rule
public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml"));
private Profiles profiles;
@Before
public void setupProfilesDao() {
FaultTolerantDatabase faultTolerantDatabase = new FaultTolerantDatabase("profilesTest",
Jdbi.create(db.getTestDatabase()),
new CircuitBreakerConfiguration());
this.profiles = new Profiles(faultTolerantDatabase);
}
@Test
public void testSetGet() {
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("123", "foo", "avatarLocation", "acommitment".getBytes());
profiles.set(uuid, profile);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(profile.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(profile.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profile.getCommitment());
}
@Test
public void testSetReplace() {
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("123", "foo", "avatarLocation", "acommitment".getBytes());
profiles.set(uuid, profile);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(profile.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(profile.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profile.getCommitment());
VersionedProfile updated = new VersionedProfile("123", "bar", "baz", "boof".getBytes());
profiles.set(uuid, updated);
retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(updated.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(updated.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profile.getCommitment());
}
@Test
public void testMultipleVersions() {
UUID uuid = UUID.randomUUID();
VersionedProfile profileOne = new VersionedProfile("123", "foo", "avatarLocation", "acommitmnet".getBytes());
VersionedProfile profileTwo = new VersionedProfile("345", "bar", "baz", "boof".getBytes());
profiles.set(uuid, profileOne);
profiles.set(uuid, profileTwo);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(profileOne.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(profileOne.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profileOne.getCommitment());
retrieved = profiles.get(uuid, "345");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(profileTwo.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(profileTwo.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profileTwo.getCommitment());
}
@Test
public void testMissing() {
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("123", "foo", "avatarLocation", "aDigest".getBytes());
profiles.set(uuid, profile);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "888");
assertThat(retrieved.isPresent()).isFalse();
}
@Test
public void testDelete() {
UUID uuid = UUID.randomUUID();
VersionedProfile profileOne = new VersionedProfile("123", "foo", "avatarLocation", "aDigest".getBytes());
VersionedProfile profileTwo = new VersionedProfile("345", "bar", "baz", "boof".getBytes());
profiles.set(uuid, profileOne);
profiles.set(uuid, profileTwo);
profiles.deleteAll(uuid);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isFalse();
retrieved = profiles.get(uuid, "345");
assertThat(retrieved.isPresent()).isFalse();
}
}