mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-22 17:08:06 +01:00
test classes moved to same packages with components they test
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.time.Duration;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class ArtControllerTest {
|
||||
private static final ArtServiceConfiguration ART_SERVICE_CONFIGURATION = new ArtServiceConfiguration(
|
||||
randomSecretBytes(32), randomSecretBytes(32), Duration.ofDays(1));
|
||||
private static final ExternalServiceCredentialsGenerator artCredentialsGenerator = ArtController.credentialsGenerator(ART_SERVICE_CONFIGURATION);
|
||||
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new ArtController(rateLimiters, artCredentialsGenerator))
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void testGetAuthToken() {
|
||||
when(rateLimiters.getArtPackLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
ExternalServiceCredentials token =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/art/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(ExternalServiceCredentials.class);
|
||||
|
||||
assertThat(token.password()).isNotEmpty();
|
||||
assertThat(token.username()).isNotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class AttachmentControllerTest {
|
||||
|
||||
private static final RateLimiter RATE_LIMITER = mock(RateLimiter.class);
|
||||
|
||||
private static final RateLimiters RATE_LIMITERS = MockUtils.buildMock(RateLimiters.class, rateLimiters ->
|
||||
when(rateLimiters.getAttachmentLimiter()).thenReturn(RATE_LIMITER));
|
||||
|
||||
public static final String RSA_PRIVATE_KEY_PEM;
|
||||
|
||||
static {
|
||||
try {
|
||||
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(1024);
|
||||
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
RSA_PRIVATE_KEY_PEM = "-----BEGIN PRIVATE KEY-----\n" +
|
||||
Base64.getMimeEncoder().encodeToString(keyPair.getPrivate().getEncoded()) + "\n" +
|
||||
"-----END PRIVATE KEY-----";
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final ResourceExtension resources;
|
||||
|
||||
static {
|
||||
try {
|
||||
resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new AttachmentControllerV2(RATE_LIMITERS, "accessKey", "accessSecret", "us-east-1", "attachmentv2-bucket"))
|
||||
.addResource(new AttachmentControllerV3(RATE_LIMITERS, "some-cdn.signal.org", "signal@example.com", 1000, "/attach-here", RSA_PRIVATE_KEY_PEM))
|
||||
.build();
|
||||
} catch (IOException | InvalidKeyException | InvalidKeySpecException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testV3Form() {
|
||||
AttachmentDescriptorV3 descriptor = resources.getJerseyTest()
|
||||
.target("/v3/attachments/form/upload")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(AttachmentDescriptorV3.class);
|
||||
|
||||
assertThat(descriptor.key()).isNotBlank();
|
||||
assertThat(descriptor.cdn()).isEqualTo(2);
|
||||
assertThat(descriptor.headers()).hasSize(3);
|
||||
assertThat(descriptor.headers()).extractingByKey("host").isEqualTo("some-cdn.signal.org");
|
||||
assertThat(descriptor.headers()).extractingByKey("x-goog-resumable").isEqualTo("start");
|
||||
assertThat(descriptor.headers()).extractingByKey("x-goog-content-length-range").isEqualTo("1,1000");
|
||||
assertThat(descriptor.signedUploadLocation()).isNotEmpty();
|
||||
assertThat(descriptor.signedUploadLocation()).contains("X-Goog-Signature");
|
||||
assertThat(descriptor.signedUploadLocation()).is(new Condition<>(x -> {
|
||||
try {
|
||||
new URL(x);
|
||||
} catch (MalformedURLException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, "convertible to a URL", (Object[]) null));
|
||||
|
||||
final URL signedUploadLocation;
|
||||
try {
|
||||
signedUploadLocation = new URL(descriptor.signedUploadLocation());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
assertThat(signedUploadLocation.getHost()).isEqualTo("some-cdn.signal.org");
|
||||
assertThat(signedUploadLocation.getPath()).startsWith("/attach-here/");
|
||||
final Map<String, String> queryParamMap = new HashMap<>();
|
||||
final String[] queryTerms = signedUploadLocation.getQuery().split("&");
|
||||
for (final String queryTerm : queryTerms) {
|
||||
final String[] keyValueArray = queryTerm.split("=", 2);
|
||||
queryParamMap.put(
|
||||
URLDecoder.decode(keyValueArray[0], StandardCharsets.UTF_8),
|
||||
URLDecoder.decode(keyValueArray[1], StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
assertThat(queryParamMap).extractingByKey("X-Goog-Algorithm").isEqualTo("GOOG4-RSA-SHA256");
|
||||
assertThat(queryParamMap).extractingByKey("X-Goog-Expires").isEqualTo("90000");
|
||||
assertThat(queryParamMap).extractingByKey("X-Goog-SignedHeaders").isEqualTo("host;x-goog-content-length-range;x-goog-resumable");
|
||||
assertThat(queryParamMap).extractingByKey("X-Goog-Date", Assertions.as(InstanceOfAssertFactories.STRING)).isNotEmpty();
|
||||
|
||||
final String credential = queryParamMap.get("X-Goog-Credential");
|
||||
String[] credentialParts = credential.split("/");
|
||||
assertThat(credentialParts).hasSize(5);
|
||||
assertThat(credentialParts[0]).isEqualTo("signal@example.com");
|
||||
assertThat(credentialParts[2]).isEqualTo("auto");
|
||||
assertThat(credentialParts[3]).isEqualTo("storage");
|
||||
assertThat(credentialParts[4]).isEqualTo("goog4_request");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testV3FormDisabled() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v3/attachments/form/upload")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testV2Form() throws IOException {
|
||||
AttachmentDescriptorV2 descriptor = resources.getJerseyTest()
|
||||
.target("/v2/attachments/form/upload")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(AttachmentDescriptorV2.class);
|
||||
|
||||
assertThat(descriptor.key()).isEqualTo(descriptor.attachmentIdString());
|
||||
assertThat(descriptor.acl()).isEqualTo("private");
|
||||
assertThat(descriptor.algorithm()).isEqualTo("AWS4-HMAC-SHA256");
|
||||
assertThat(descriptor.attachmentId()).isGreaterThan(0);
|
||||
assertThat(String.valueOf(descriptor.attachmentId())).isEqualTo(descriptor.attachmentIdString());
|
||||
|
||||
String[] credentialParts = descriptor.credential().split("/");
|
||||
|
||||
assertThat(credentialParts[0]).isEqualTo("accessKey");
|
||||
assertThat(credentialParts[2]).isEqualTo("us-east-1");
|
||||
assertThat(credentialParts[3]).isEqualTo("s3");
|
||||
assertThat(credentialParts[4]).isEqualTo("aws4_request");
|
||||
|
||||
assertThat(descriptor.date()).isNotBlank();
|
||||
assertThat(descriptor.policy()).isNotBlank();
|
||||
assertThat(descriptor.signature()).isNotBlank();
|
||||
|
||||
assertThat(new String(Base64.getDecoder().decode(descriptor.policy()))).contains("[\"content-length-range\", 1, 104857600]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testV2FormDisabled() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v2/attachments/form/upload")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.signal.libsignal.protocol.util.Hex;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequestContext;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.GetCreateCallLinkCredentialsRequest;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
public class CallLinkControllerTest {
|
||||
private static final GenericServerSecretParams genericServerSecretParams = GenericServerSecretParams.generate();
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private static final RateLimiter createCallLinkLimiter = mock(RateLimiter.class);
|
||||
private static final byte[] roomId = Hex.fromStringCondensedAssert("c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7");
|
||||
private static final CreateCallLinkCredentialRequestContext createCallLinkRequestContext = CreateCallLinkCredentialRequestContext.forRoom(roomId);
|
||||
private static final byte[] createCallLinkRequestSerialized = createCallLinkRequestContext.getRequest().serialize();
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.addProvider(new RateLimitExceededExceptionMapper())
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new CallLinkController(rateLimiters, genericServerSecretParams))
|
||||
.build();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
when(rateLimiters.getCreateCallLinkLimiter()).thenReturn(createCallLinkLimiter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreateAuth() {
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target("/v1/call-link/create-auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.json(new GetCreateCallLinkCredentialsRequest(createCallLinkRequestSerialized)))) {
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreateAuthInvalidInput() {
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target("/v1/call-link/create-auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.json(new GetCreateCallLinkCredentialsRequest(new byte[10])))) {
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreateAuthInvalidAuth() {
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target("/v1/call-link/create-auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.json(new GetCreateCallLinkCredentialsRequest(createCallLinkRequestSerialized)))) {
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreateAuthInvalidRequest() {
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target("/v1/call-link/create-auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.json(""))) {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreateAuthInvalidInputEmptyRequestBody() {
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target("/v1/call-link/create-auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.json("{}"))) {
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreateAuthInvalidInputEmptyField() {
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target("/v1/call-link/create-auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.json("{\"createCallLinkCredentialRequest\": \"\"}"))) {
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreateAuthRatelimited() throws RateLimitExceededException{
|
||||
doThrow(new RateLimitExceededException(null, false))
|
||||
.when(createCallLinkLimiter).validate(AuthHelper.VALID_UUID);
|
||||
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target("/v1/call-link/create-auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.json(new GetCreateCallLinkCredentialsRequest(createCallLinkRequestSerialized)))) {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(429);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
import java.util.stream.Stream;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations;
|
||||
import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
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.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class CertificateControllerTest {
|
||||
|
||||
private static final String caPublicKey = "BWh+UOhT1hD8bkb+MFRvb6tVqhoG8YYGCzOd7mgjo8cV";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String caPrivateKey = "EO3Mnf0kfVlVnwSaqPoQnAxhnnGL1JTdXqktCKEe9Eo=";
|
||||
|
||||
private static final String signingCertificate = "CiUIDBIhBbTz4h1My+tt+vw+TVscgUe/DeHS0W02tPWAWbTO2xc3EkD+go4bJnU0AcnFfbOLKoiBfCzouZtDYMOVi69rE7r4U9cXREEqOkUmU2WJBjykAxWPCcSTmVTYHDw7hkSp/puG";
|
||||
private static final String signingKey = "ABOxG29xrfq4E7IrW11Eg7+HBbtba9iiS0500YoBjn4=";
|
||||
|
||||
private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate();
|
||||
|
||||
private static final GenericServerSecretParams genericServerSecretParams = GenericServerSecretParams.generate();
|
||||
private static final CertificateGenerator certificateGenerator;
|
||||
private static final ServerZkAuthOperations serverZkAuthOperations;
|
||||
private static final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
|
||||
|
||||
static {
|
||||
try {
|
||||
certificateGenerator = new CertificateGenerator(Base64.getDecoder().decode(signingCertificate),
|
||||
Curve.decodePrivatePoint(Base64.getDecoder().decode(signingKey)), 1);
|
||||
serverZkAuthOperations = new ServerZkAuthOperations(serverSecretParams);
|
||||
} catch (IOException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new CertificateController(certificateGenerator, serverZkAuthOperations, genericServerSecretParams, clock))
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void testValidCertificate() throws Exception {
|
||||
DeliveryCertificate certificateObject = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(DeliveryCertificate.class);
|
||||
|
||||
SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate());
|
||||
SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom(
|
||||
certificateHolder.getCertificate());
|
||||
|
||||
ServerCertificate serverCertificateHolder = certificate.getSigner();
|
||||
ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom(
|
||||
serverCertificateHolder.getCertificate());
|
||||
|
||||
assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0),
|
||||
certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray()));
|
||||
assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.getDecoder().decode(caPublicKey), 0),
|
||||
serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray()));
|
||||
|
||||
assertEquals(certificate.getSender(), AuthHelper.VALID_NUMBER);
|
||||
assertEquals(certificate.getSenderDevice(), 1L);
|
||||
assertTrue(certificate.hasSenderUuid());
|
||||
assertEquals(AuthHelper.VALID_UUID.toString(), certificate.getSenderUuid());
|
||||
assertArrayEquals(certificate.getIdentityKey().toByteArray(), AuthHelper.VALID_IDENTITY.serialize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidCertificateWithUuid() throws Exception {
|
||||
DeliveryCertificate certificateObject = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.queryParam("includeUuid", "true")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(DeliveryCertificate.class);
|
||||
|
||||
SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate());
|
||||
SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom(
|
||||
certificateHolder.getCertificate());
|
||||
|
||||
ServerCertificate serverCertificateHolder = certificate.getSigner();
|
||||
ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom(
|
||||
serverCertificateHolder.getCertificate());
|
||||
|
||||
assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0),
|
||||
certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray()));
|
||||
assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.getDecoder().decode(caPublicKey), 0),
|
||||
serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray()));
|
||||
|
||||
assertEquals(certificate.getSender(), AuthHelper.VALID_NUMBER);
|
||||
assertEquals(certificate.getSenderDevice(), 1L);
|
||||
assertEquals(certificate.getSenderUuid(), AuthHelper.VALID_UUID.toString());
|
||||
assertArrayEquals(certificate.getIdentityKey().toByteArray(), AuthHelper.VALID_IDENTITY.serialize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidCertificateWithUuidNoE164() throws Exception {
|
||||
DeliveryCertificate certificateObject = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.queryParam("includeUuid", "true")
|
||||
.queryParam("includeE164", "false")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(DeliveryCertificate.class);
|
||||
|
||||
SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate());
|
||||
SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom(
|
||||
certificateHolder.getCertificate());
|
||||
|
||||
ServerCertificate serverCertificateHolder = certificate.getSigner();
|
||||
ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom(
|
||||
serverCertificateHolder.getCertificate());
|
||||
|
||||
assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0),
|
||||
certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray()));
|
||||
assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.getDecoder().decode(caPublicKey), 0),
|
||||
serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray()));
|
||||
|
||||
assertTrue(StringUtils.isBlank(certificate.getSender()));
|
||||
assertEquals(certificate.getSenderDevice(), 1L);
|
||||
assertEquals(certificate.getSenderUuid(), AuthHelper.VALID_UUID.toString());
|
||||
assertArrayEquals(certificate.getIdentityKey().toByteArray(), AuthHelper.VALID_IDENTITY.serialize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBadAuthentication() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertEquals(response.getStatus(), 401);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testNoAuthentication() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.get();
|
||||
|
||||
assertEquals(response.getStatus(), 401);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testUnidentifiedAuthentication() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1234".getBytes()))
|
||||
.get();
|
||||
|
||||
assertEquals(response.getStatus(), 401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisabledAuthentication() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertEquals(response.getStatus(), 401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSingleGroupCredential() {
|
||||
final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
final GroupCredentials credentials = resources.getJerseyTest()
|
||||
.target("/v1/certificate/auth/group")
|
||||
.queryParam("redemptionStartSeconds", startOfDay.getEpochSecond())
|
||||
.queryParam("redemptionEndSeconds", startOfDay.getEpochSecond())
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(GroupCredentials.class);
|
||||
|
||||
assertEquals(1, credentials.credentials().size());
|
||||
assertEquals(1, credentials.callLinkAuthCredentials().size());
|
||||
|
||||
assertEquals(AuthHelper.VALID_PNI, credentials.pni());
|
||||
assertEquals(startOfDay.getEpochSecond(), credentials.credentials().get(0).redemptionTime());
|
||||
assertEquals(startOfDay.getEpochSecond(), credentials.callLinkAuthCredentials().get(0).redemptionTime());
|
||||
|
||||
final ClientZkAuthOperations clientZkAuthOperations =
|
||||
new ClientZkAuthOperations(serverSecretParams.getPublicParams());
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
clientZkAuthOperations.receiveAuthCredentialWithPni(
|
||||
AuthHelper.VALID_UUID,
|
||||
AuthHelper.VALID_PNI,
|
||||
(int) startOfDay.getEpochSecond(),
|
||||
new AuthCredentialWithPniResponse(credentials.credentials().get(0).credential()));
|
||||
});
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
new CallLinkAuthCredentialResponse(credentials.callLinkAuthCredentials().get(0).credential())
|
||||
.receive(AuthHelper.VALID_UUID, startOfDay, genericServerSecretParams.getPublicParams());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetWeekLongGroupCredentials() {
|
||||
final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
final GroupCredentials credentials = resources.getJerseyTest()
|
||||
.target("/v1/certificate/auth/group")
|
||||
.queryParam("redemptionStartSeconds", startOfDay.getEpochSecond())
|
||||
.queryParam("redemptionEndSeconds", startOfDay.plus(Duration.ofDays(7)).getEpochSecond())
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(GroupCredentials.class);
|
||||
|
||||
assertEquals(AuthHelper.VALID_PNI, credentials.pni());
|
||||
assertEquals(8, credentials.credentials().size());
|
||||
assertEquals(8, credentials.callLinkAuthCredentials().size());
|
||||
|
||||
final ClientZkAuthOperations clientZkAuthOperations =
|
||||
new ClientZkAuthOperations(serverSecretParams.getPublicParams());
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
final Instant redemptionTime = startOfDay.plus(Duration.ofDays(i));
|
||||
assertEquals(redemptionTime.getEpochSecond(), credentials.credentials().get(i).redemptionTime());
|
||||
assertEquals(redemptionTime.getEpochSecond(), credentials.callLinkAuthCredentials().get(i).redemptionTime());
|
||||
|
||||
final int index = i;
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
clientZkAuthOperations.receiveAuthCredentialWithPni(
|
||||
AuthHelper.VALID_UUID,
|
||||
AuthHelper.VALID_PNI,
|
||||
redemptionTime.getEpochSecond(),
|
||||
new AuthCredentialWithPniResponse(credentials.credentials().get(index).credential()));
|
||||
});
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
new CallLinkAuthCredentialResponse(credentials.callLinkAuthCredentials().get(index).credential())
|
||||
.receive(AuthHelper.VALID_UUID, redemptionTime, genericServerSecretParams.getPublicParams());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testBadRedemptionTimes(final Instant redemptionStart, final Instant redemptionEnd) {
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("/v1/certificate/auth/group")
|
||||
.queryParam("redemptionStartSeconds", redemptionStart.getEpochSecond())
|
||||
.queryParam("redemptionEndSeconds", redemptionEnd.getEpochSecond())
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertEquals(400, response.getStatus());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> testBadRedemptionTimes() {
|
||||
return Stream.of(
|
||||
// Start is after end
|
||||
Arguments.of(clock.instant().plus(Duration.ofDays(1)), clock.instant()),
|
||||
|
||||
// Start is in the past
|
||||
Arguments.of(clock.instant().minus(Duration.ofDays(1)), clock.instant()),
|
||||
|
||||
// End is too far in the future
|
||||
Arguments.of(clock.instant(),
|
||||
clock.instant().plus(CertificateController.MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1))),
|
||||
|
||||
// Start is not at a day boundary
|
||||
Arguments.of(clock.instant().plusSeconds(17), clock.instant().plus(Duration.ofDays(1))),
|
||||
|
||||
// End is not at a day boundary
|
||||
Arguments.of(clock.instant(), clock.instant().plusSeconds(17))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
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.ArgumentCaptor;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.LinkDeviceRequest;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class DeviceControllerTest {
|
||||
|
||||
@Path("/v1/devices")
|
||||
static class DumbVerificationDeviceController extends DeviceController {
|
||||
|
||||
public DumbVerificationDeviceController(StoredVerificationCodeManager pendingDevices,
|
||||
AccountsManager accounts,
|
||||
MessagesManager messages,
|
||||
KeysManager keys,
|
||||
RateLimiters rateLimiters,
|
||||
Map<String, Integer> deviceConfiguration) {
|
||||
super(pendingDevices, accounts, messages, keys, rateLimiters, deviceConfiguration);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VerificationCode generateVerificationCode() {
|
||||
return new VerificationCode(5678901);
|
||||
}
|
||||
}
|
||||
|
||||
private static StoredVerificationCodeManager pendingDevicesManager = mock(StoredVerificationCodeManager.class);
|
||||
private static AccountsManager accountsManager = mock(AccountsManager.class);
|
||||
private static MessagesManager messagesManager = mock(MessagesManager.class);
|
||||
private static KeysManager keysManager = mock(KeysManager.class);
|
||||
private static RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private static RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||
private static Account account = mock(Account.class);
|
||||
private static Account maxedAccount = mock(Account.class);
|
||||
private static Device masterDevice = mock(Device.class);
|
||||
private static ClientPresenceManager clientPresenceManager = mock(ClientPresenceManager.class);
|
||||
|
||||
private static Map<String, Integer> deviceConfiguration = new HashMap<>();
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addProvider(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager))
|
||||
.addProvider(new DeviceLimitExceededExceptionMapper())
|
||||
.addResource(new DumbVerificationDeviceController(pendingDevicesManager,
|
||||
accountsManager,
|
||||
messagesManager,
|
||||
keysManager,
|
||||
rateLimiters,
|
||||
deviceConfiguration))
|
||||
.build();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getAllocateDeviceLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getVerifyDeviceLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
when(masterDevice.getId()).thenReturn(1L);
|
||||
|
||||
when(account.getNextDeviceId()).thenReturn(42L);
|
||||
when(account.getNumber()).thenReturn(AuthHelper.VALID_NUMBER);
|
||||
when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID);
|
||||
when(account.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI);
|
||||
when(account.isEnabled()).thenReturn(false);
|
||||
when(account.isSenderKeySupported()).thenReturn(true);
|
||||
when(account.isAnnouncementGroupSupported()).thenReturn(true);
|
||||
when(account.isChangeNumberSupported()).thenReturn(true);
|
||||
when(account.isPniSupported()).thenReturn(true);
|
||||
when(account.isStoriesSupported()).thenReturn(true);
|
||||
when(account.isGiftBadgesSupported()).thenReturn(true);
|
||||
when(account.isPaymentActivationSupported()).thenReturn(false);
|
||||
|
||||
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(
|
||||
Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis(), null, null)));
|
||||
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.empty());
|
||||
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount));
|
||||
|
||||
AccountsHelper.setupMockUpdate(accountsManager);
|
||||
|
||||
when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(keysManager.delete(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
reset(
|
||||
pendingDevicesManager,
|
||||
accountsManager,
|
||||
messagesManager,
|
||||
keysManager,
|
||||
rateLimiters,
|
||||
rateLimiter,
|
||||
account,
|
||||
maxedAccount,
|
||||
masterDevice,
|
||||
clientPresenceManager
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validDeviceRegisterTest() {
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
final Device existingDevice = mock(Device.class);
|
||||
when(existingDevice.getId()).thenReturn(Device.MASTER_ID);
|
||||
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
|
||||
|
||||
VerificationCode deviceCode = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
||||
|
||||
DeviceResponse response = resources.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.entity(new AccountAttributes(false, 1234, null,
|
||||
null, true, null),
|
||||
MediaType.APPLICATION_JSON_TYPE),
|
||||
DeviceResponse.class);
|
||||
|
||||
assertThat(response.getDeviceId()).isEqualTo(42L);
|
||||
|
||||
verify(pendingDevicesManager).remove(AuthHelper.VALID_NUMBER);
|
||||
verify(messagesManager).clear(eq(AuthHelper.VALID_UUID), eq(42L));
|
||||
verify(clientPresenceManager).disconnectPresence(AuthHelper.VALID_UUID, Device.MASTER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyDeviceWithNullAccountAttributes() {
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
final Device existingDevice = mock(Device.class);
|
||||
when(existingDevice.getId()).thenReturn(Device.MASTER_ID);
|
||||
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
|
||||
|
||||
VerificationCode deviceCode = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
||||
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.json(""));
|
||||
|
||||
assertThat(response.getStatus()).isNotEqualTo(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyDeviceTokenBadCredentials() {
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", "This is not a valid authorization header")
|
||||
.put(Entity.entity(new AccountAttributes(false, 1234, null,
|
||||
null, true, null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertEquals(401, response.getStatus());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
void linkDeviceAtomic(final boolean fetchesMessages,
|
||||
final Optional<ApnRegistrationId> apnRegistrationId,
|
||||
final Optional<GcmRegistrationId> gcmRegistrationId,
|
||||
final Optional<String> expectedApnsToken,
|
||||
final Optional<String> expectedApnsVoipToken,
|
||||
final Optional<String> expectedGcmToken) {
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
final Device existingDevice = mock(Device.class);
|
||||
when(existingDevice.getId()).thenReturn(Device.MASTER_ID);
|
||||
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
|
||||
|
||||
VerificationCode deviceCode = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
||||
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey;
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey;
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey;
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey;
|
||||
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
|
||||
when(account.getIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
|
||||
when(account.getPhoneNumberIdentityKey()).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
|
||||
|
||||
when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
final LinkDeviceRequest request = new LinkDeviceRequest("5678901",
|
||||
new AccountAttributes(fetchesMessages, 1234, null, null, true, null),
|
||||
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnRegistrationId, gcmRegistrationId));
|
||||
|
||||
final DeviceResponse response = resources.getJerseyTest()
|
||||
.target("/v1/devices/link")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE), DeviceResponse.class);
|
||||
|
||||
assertThat(response.getDeviceId()).isEqualTo(42L);
|
||||
|
||||
final ArgumentCaptor<Device> deviceCaptor = ArgumentCaptor.forClass(Device.class);
|
||||
verify(account).addDevice(deviceCaptor.capture());
|
||||
|
||||
final Device device = deviceCaptor.getValue();
|
||||
|
||||
assertEquals(aciSignedPreKey.get(), device.getSignedPreKey());
|
||||
assertEquals(pniSignedPreKey.get(), device.getPhoneNumberIdentitySignedPreKey());
|
||||
assertEquals(fetchesMessages, device.getFetchesMessages());
|
||||
|
||||
expectedApnsToken.ifPresentOrElse(expectedToken -> assertEquals(expectedToken, device.getApnId()),
|
||||
() -> assertNull(device.getApnId()));
|
||||
|
||||
expectedApnsVoipToken.ifPresentOrElse(expectedToken -> assertEquals(expectedToken, device.getVoipApnId()),
|
||||
() -> assertNull(device.getVoipApnId()));
|
||||
|
||||
expectedGcmToken.ifPresentOrElse(expectedToken -> assertEquals(expectedToken, device.getGcmId()),
|
||||
() -> assertNull(device.getGcmId()));
|
||||
|
||||
verify(pendingDevicesManager).remove(AuthHelper.VALID_NUMBER);
|
||||
verify(messagesManager).clear(eq(AuthHelper.VALID_UUID), eq(42L));
|
||||
verify(clientPresenceManager).disconnectPresence(AuthHelper.VALID_UUID, Device.MASTER_ID);
|
||||
verify(keysManager).storeEcSignedPreKeys(AuthHelper.VALID_UUID, Map.of(response.getDeviceId(), aciSignedPreKey.get()));
|
||||
verify(keysManager).storeEcSignedPreKeys(AuthHelper.VALID_PNI, Map.of(response.getDeviceId(), pniSignedPreKey.get()));
|
||||
verify(keysManager).storePqLastResort(AuthHelper.VALID_UUID, Map.of(response.getDeviceId(), aciPqLastResortPreKey.get()));
|
||||
verify(keysManager).storePqLastResort(AuthHelper.VALID_PNI, Map.of(response.getDeviceId(), pniPqLastResortPreKey.get()));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> linkDeviceAtomic() {
|
||||
final String apnsToken = "apns-token";
|
||||
final String apnsVoipToken = "apns-voip-token";
|
||||
final String gcmToken = "gcm-token";
|
||||
|
||||
return Stream.of(
|
||||
Arguments.of(true, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()),
|
||||
Arguments.of(false, Optional.of(new ApnRegistrationId(apnsToken, null)), Optional.empty(), Optional.of(apnsToken), Optional.empty(), Optional.empty()),
|
||||
Arguments.of(false, Optional.of(new ApnRegistrationId(apnsToken, apnsVoipToken)), Optional.empty(), Optional.of(apnsToken), Optional.of(apnsVoipToken), Optional.empty()),
|
||||
Arguments.of(false, Optional.empty(), Optional.of(new GcmRegistrationId(gcmToken)), Optional.empty(), Optional.empty(), Optional.of(gcmToken))
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
void linkDeviceAtomicConflictingChannel(final boolean fetchesMessages,
|
||||
final Optional<ApnRegistrationId> apnRegistrationId,
|
||||
final Optional<GcmRegistrationId> gcmRegistrationId) {
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
final Device existingDevice = mock(Device.class);
|
||||
when(existingDevice.getId()).thenReturn(Device.MASTER_ID);
|
||||
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
|
||||
|
||||
VerificationCode deviceCode = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
||||
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey;
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey;
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey;
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey;
|
||||
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
|
||||
when(account.getIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
|
||||
when(account.getPhoneNumberIdentityKey()).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
|
||||
|
||||
final LinkDeviceRequest request = new LinkDeviceRequest("5678901",
|
||||
new AccountAttributes(fetchesMessages, 1234, null, null, true, null),
|
||||
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnRegistrationId, gcmRegistrationId));
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/link")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) {
|
||||
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> linkDeviceAtomicConflictingChannel() {
|
||||
return Stream.of(
|
||||
Arguments.of(true, Optional.of(new ApnRegistrationId("apns-token", null)), Optional.of(new GcmRegistrationId("gcm-token"))),
|
||||
Arguments.of(true, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-token"))),
|
||||
Arguments.of(true, Optional.of(new ApnRegistrationId("apns-token", null)), Optional.empty()),
|
||||
Arguments.of(false, Optional.of(new ApnRegistrationId("apns-token", null)), Optional.of(new GcmRegistrationId("gcm-token")))
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
void linkDeviceAtomicMissingProperty(final IdentityKey aciIdentityKey,
|
||||
final IdentityKey pniIdentityKey,
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey,
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey,
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey) {
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
final Device existingDevice = mock(Device.class);
|
||||
when(existingDevice.getId()).thenReturn(Device.MASTER_ID);
|
||||
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
|
||||
|
||||
VerificationCode deviceCode = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
||||
|
||||
when(account.getIdentityKey()).thenReturn(aciIdentityKey);
|
||||
when(account.getPhoneNumberIdentityKey()).thenReturn(pniIdentityKey);
|
||||
|
||||
final LinkDeviceRequest request = new LinkDeviceRequest("5678901",
|
||||
new AccountAttributes(true, 1234, null, null, true, null),
|
||||
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.empty()));
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/link")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) {
|
||||
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> linkDeviceAtomicMissingProperty() {
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
|
||||
final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey());
|
||||
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||
|
||||
return Stream.of(
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, Optional.empty(), pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, Optional.empty(), aciPqLastResortPreKey, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, Optional.empty(), pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, Optional.empty())
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void linkDeviceAtomicInvalidSignature(final IdentityKey aciIdentityKey,
|
||||
final IdentityKey pniIdentityKey,
|
||||
final ECSignedPreKey aciSignedPreKey,
|
||||
final ECSignedPreKey pniSignedPreKey,
|
||||
final KEMSignedPreKey aciPqLastResortPreKey,
|
||||
final KEMSignedPreKey pniPqLastResortPreKey) {
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
final Device existingDevice = mock(Device.class);
|
||||
when(existingDevice.getId()).thenReturn(Device.MASTER_ID);
|
||||
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
|
||||
|
||||
VerificationCode deviceCode = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
||||
|
||||
when(account.getIdentityKey()).thenReturn(aciIdentityKey);
|
||||
when(account.getPhoneNumberIdentityKey()).thenReturn(pniIdentityKey);
|
||||
|
||||
final LinkDeviceRequest request = new LinkDeviceRequest("5678901",
|
||||
new AccountAttributes(true, 1234, null, null, true, null),
|
||||
new DeviceActivationRequest(Optional.of(aciSignedPreKey), Optional.of(pniSignedPreKey), Optional.of(aciPqLastResortPreKey), Optional.of(pniPqLastResortPreKey), Optional.empty(), Optional.empty()));
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/link")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) {
|
||||
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> linkDeviceAtomicInvalidSignature() {
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
final ECSignedPreKey aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
|
||||
final ECSignedPreKey pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
|
||||
final KEMSignedPreKey aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
|
||||
final KEMSignedPreKey pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
|
||||
|
||||
final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey());
|
||||
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||
|
||||
return Stream.of(
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, ecSignedPreKeyWithBadSignature(aciSignedPreKey), pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, ecSignedPreKeyWithBadSignature(pniSignedPreKey), aciPqLastResortPreKey, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, kemSignedPreKeyWithBadSignature(aciPqLastResortPreKey), pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, kemSignedPreKeyWithBadSignature(pniPqLastResortPreKey))
|
||||
);
|
||||
}
|
||||
|
||||
private static ECSignedPreKey ecSignedPreKeyWithBadSignature(final ECSignedPreKey signedPreKey) {
|
||||
return new ECSignedPreKey(signedPreKey.keyId(),
|
||||
signedPreKey.publicKey(),
|
||||
"incorrect-signature".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static KEMSignedPreKey kemSignedPreKeyWithBadSignature(final KEMSignedPreKey signedPreKey) {
|
||||
return new KEMSignedPreKey(signedPreKey.keyId(),
|
||||
signedPreKey.publicKey(),
|
||||
"incorrect-signature".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledDeviceRegisterTest() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidDeviceRegisterTest() {
|
||||
VerificationCode deviceCode = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
||||
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/5678902")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.entity(new AccountAttributes(false, 1234, null, null, true, null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
verifyNoMoreInteractions(messagesManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oldDeviceRegisterTest() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/1112223")
|
||||
.request()
|
||||
.header("Authorization",
|
||||
AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
|
||||
.put(Entity.entity(new AccountAttributes(false, 1234, null, null, true, null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
verifyNoMoreInteractions(messagesManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxDevicesTest() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/provisioning/code")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
|
||||
.get();
|
||||
|
||||
assertEquals(411, response.getStatus());
|
||||
verifyNoMoreInteractions(messagesManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void longNameTest() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.entity(new AccountAttributes(false, 1234,
|
||||
"this is a really long name that is longer than 80 characters it's so long that it's even longer than 204 characters. that's a lot of characters. we're talking lots and lots and lots of characters. 12345678",
|
||||
null, true, null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertEquals(response.getStatus(), 422);
|
||||
verifyNoMoreInteractions(messagesManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceDowngradeSenderKeyTest() {
|
||||
DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, false, true,
|
||||
true, true, true, true, true);
|
||||
AccountAttributes accountAttributes =
|
||||
new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
|
||||
deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true, true);
|
||||
accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceDowngradeAnnouncementGroupTest() {
|
||||
DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, false,
|
||||
true, true, true, true, true);
|
||||
AccountAttributes accountAttributes =
|
||||
new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
|
||||
deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true, true);
|
||||
accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceDowngradeChangeNumberTest() {
|
||||
DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true,
|
||||
false, true, true, true, true);
|
||||
AccountAttributes accountAttributes =
|
||||
new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization",
|
||||
AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
|
||||
deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true, true);
|
||||
accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization",
|
||||
AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceDowngradePniTest() {
|
||||
DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, true,
|
||||
false, true, true, true);
|
||||
AccountAttributes accountAttributes =
|
||||
new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
|
||||
deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true, true);
|
||||
accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization",
|
||||
AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceDowngradeStoriesTest() {
|
||||
DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, true,
|
||||
true, false, true, true);
|
||||
AccountAttributes accountAttributes =
|
||||
new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization",
|
||||
AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
|
||||
deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true, true);
|
||||
accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization",
|
||||
AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceDowngradeGiftBadgesTest() {
|
||||
DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, false, true);
|
||||
AccountAttributes accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
|
||||
deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true, true);
|
||||
accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization",
|
||||
AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putCapabilitiesSuccessTest() {
|
||||
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true, true);
|
||||
final Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/capabilities")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(deviceCapabilities, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
assertThat(response.hasEntity()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void putCapabilitiesFailureTest() {
|
||||
final Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/capabilities")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.json(""));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void deviceDowngradePaymentActivationTest(boolean paymentActivation) {
|
||||
// Update when we start returning true value of capability & restricting downgrades
|
||||
DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true, paymentActivation);
|
||||
AccountAttributes accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities);
|
||||
Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceRemovalClearsMessagesAndKeys() {
|
||||
|
||||
// this is a static mock, so it might have previous invocations
|
||||
clearInvocations(AuthHelper.VALID_ACCOUNT);
|
||||
|
||||
final long deviceId = 2;
|
||||
|
||||
final Response response = resources
|
||||
.getJerseyTest()
|
||||
.target("/v1/devices/" + deviceId)
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
|
||||
.delete();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
assertThat(response.hasEntity()).isFalse();
|
||||
|
||||
verify(messagesManager, times(2)).clear(AuthHelper.VALID_UUID, deviceId);
|
||||
verify(accountsManager, times(1)).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
||||
verify(AuthHelper.VALID_ACCOUNT).removeDevice(deviceId);
|
||||
verify(keysManager).delete(AuthHelper.VALID_UUID, deviceId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.whispersystems.textsecuregcm.util.MockUtils.secretBytesOf;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfiguration;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
class DirectoryControllerV2Test {
|
||||
|
||||
@Test
|
||||
void testAuthToken() {
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator = DirectoryV2Controller.credentialsGenerator(
|
||||
new DirectoryV2ClientConfiguration(secretBytesOf(0x01), secretBytesOf(0x02)),
|
||||
Clock.fixed(Instant.ofEpochSecond(1633738643L), ZoneId.of("Etc/UTC"))
|
||||
);
|
||||
|
||||
final DirectoryV2Controller controller = new DirectoryV2Controller(credentialsGenerator);
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
final UUID uuid = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
when(account.getUuid()).thenReturn(uuid);
|
||||
|
||||
final ExternalServiceCredentials credentials = (ExternalServiceCredentials) controller.getAuthToken(
|
||||
new AuthenticatedAccount(() -> new Pair<>(account, mock(Device.class)))).getEntity();
|
||||
|
||||
assertEquals(credentials.username(), "d369bc712e2e0dd36258");
|
||||
assertEquals(credentials.password(), "1633738643:4433b0fab41f25f79dd4");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
|
||||
import org.whispersystems.textsecuregcm.entities.RedeemReceiptRequest;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||
|
||||
class DonationControllerTest {
|
||||
|
||||
private static final long nowEpochSeconds = 1_500_000_000L;
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
static BadgesConfiguration getBadgesConfiguration() {
|
||||
return new BadgesConfiguration(
|
||||
List.of(
|
||||
new BadgeConfiguration("TEST", "other", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
|
||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))),
|
||||
new BadgeConfiguration("TEST1", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
|
||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))),
|
||||
new BadgeConfiguration("TEST2", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
|
||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))),
|
||||
new BadgeConfiguration("TEST3", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
|
||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))),
|
||||
List.of("TEST"),
|
||||
Map.of(1L, "TEST1", 2L, "TEST2", 3L, "TEST3"));
|
||||
}
|
||||
|
||||
final Clock clock = TestClock.pinned(Instant.ofEpochSecond(nowEpochSeconds));
|
||||
ServerZkReceiptOperations zkReceiptOperations;
|
||||
RedeemedReceiptsManager redeemedReceiptsManager;
|
||||
AccountsManager accountsManager;
|
||||
byte[] receiptSerialBytes;
|
||||
ReceiptSerial receiptSerial;
|
||||
byte[] presentation;
|
||||
DonationController.ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory;
|
||||
ReceiptCredentialPresentation receiptCredentialPresentation;
|
||||
ResourceExtension resources;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws Throwable {
|
||||
zkReceiptOperations = mock(ServerZkReceiptOperations.class);
|
||||
redeemedReceiptsManager = mock(RedeemedReceiptsManager.class);
|
||||
accountsManager = mock(AccountsManager.class);
|
||||
AccountsHelper.setupMockUpdate(accountsManager);
|
||||
receiptSerialBytes = new byte[ReceiptSerial.SIZE];
|
||||
SECURE_RANDOM.nextBytes(receiptSerialBytes);
|
||||
receiptSerial = new ReceiptSerial(receiptSerialBytes);
|
||||
presentation = new byte[25];
|
||||
SECURE_RANDOM.nextBytes(presentation);
|
||||
receiptCredentialPresentationFactory = mock(DonationController.ReceiptCredentialPresentationFactory.class);
|
||||
receiptCredentialPresentation = mock(ReceiptCredentialPresentation.class);
|
||||
|
||||
try {
|
||||
when(receiptCredentialPresentationFactory.build(presentation)).thenReturn(receiptCredentialPresentation);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager,
|
||||
getBadgesConfiguration(), receiptCredentialPresentationFactory))
|
||||
.build();
|
||||
resources.before();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() throws Throwable {
|
||||
resources.after();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRedeemReceipt() {
|
||||
when(receiptCredentialPresentation.getReceiptSerial()).thenReturn(receiptSerial);
|
||||
final long receiptLevel = 1L;
|
||||
when(receiptCredentialPresentation.getReceiptLevel()).thenReturn(receiptLevel);
|
||||
final long receiptExpiration = nowEpochSeconds + 86400 * 30;
|
||||
when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(receiptExpiration);
|
||||
when(redeemedReceiptsManager.put(same(receiptSerial), eq(receiptExpiration), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn(
|
||||
CompletableFuture.completedFuture(Boolean.TRUE));
|
||||
when(accountsManager.getByAccountIdentifier(eq(AuthHelper.VALID_UUID))).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
RedeemReceiptRequest request = new RedeemReceiptRequest(presentation, true, true);
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/donation/redeem-receipt")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
verify(AuthHelper.VALID_ACCOUNT).addBadge(same(clock), eq(new AccountBadge("TEST1", Instant.ofEpochSecond(receiptExpiration), true)));
|
||||
verify(AuthHelper.VALID_ACCOUNT).makeBadgePrimaryIfExists(same(clock), eq("TEST1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRedeemReceiptAlreadyRedeemedWithDifferentParameters() {
|
||||
when(receiptCredentialPresentation.getReceiptSerial()).thenReturn(receiptSerial);
|
||||
final long receiptLevel = 1L;
|
||||
when(receiptCredentialPresentation.getReceiptLevel()).thenReturn(receiptLevel);
|
||||
final long receiptExpiration = nowEpochSeconds + 86400 * 30;
|
||||
when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(receiptExpiration);
|
||||
when(redeemedReceiptsManager.put(same(receiptSerial), eq(receiptExpiration), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn(
|
||||
CompletableFuture.completedFuture(Boolean.FALSE));
|
||||
|
||||
RedeemReceiptRequest request = new RedeemReceiptRequest(presentation, true, true);
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/donation/redeem-receipt")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
assertThat(response.readEntity(String.class)).isEqualTo("receipt serial is already redeemed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRedeemReceiptBadCredentialPresentation() throws InvalidInputException {
|
||||
when(receiptCredentialPresentationFactory.build(any())).thenThrow(new InvalidInputException());
|
||||
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("/v1/donation/redeem-receipt")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(Entity.entity(new RedeemReceiptRequest(presentation, true, true), MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,950 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyState;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class KeysControllerTest {
|
||||
|
||||
private static final String EXISTS_NUMBER = "+14152222222";
|
||||
private static final UUID EXISTS_UUID = UUID.randomUUID();
|
||||
private static final UUID EXISTS_PNI = UUID.randomUUID();
|
||||
|
||||
private static final String NOT_EXISTS_NUMBER = "+14152222220";
|
||||
private static final UUID NOT_EXISTS_UUID = UUID.randomUUID();
|
||||
|
||||
private static final int SAMPLE_REGISTRATION_ID = 999;
|
||||
private static final int SAMPLE_REGISTRATION_ID2 = 1002;
|
||||
private static final int SAMPLE_REGISTRATION_ID4 = 1555;
|
||||
|
||||
private static final int SAMPLE_PNI_REGISTRATION_ID = 1717;
|
||||
|
||||
private final ECKeyPair IDENTITY_KEY_PAIR = Curve.generateKeyPair();
|
||||
private final IdentityKey IDENTITY_KEY = new IdentityKey(IDENTITY_KEY_PAIR.getPublicKey());
|
||||
|
||||
private final ECKeyPair PNI_IDENTITY_KEY_PAIR = Curve.generateKeyPair();
|
||||
private final IdentityKey PNI_IDENTITY_KEY = new IdentityKey(PNI_IDENTITY_KEY_PAIR.getPublicKey());
|
||||
|
||||
private final ECPreKey SAMPLE_KEY = KeysHelper.ecPreKey(1234);
|
||||
private final ECPreKey SAMPLE_KEY2 = KeysHelper.ecPreKey(5667);
|
||||
private final ECPreKey SAMPLE_KEY3 = KeysHelper.ecPreKey(334);
|
||||
private final ECPreKey SAMPLE_KEY4 = KeysHelper.ecPreKey(336);
|
||||
|
||||
private final ECPreKey SAMPLE_KEY_PNI = KeysHelper.ecPreKey(7777);
|
||||
|
||||
private final KEMSignedPreKey SAMPLE_PQ_KEY = KeysHelper.signedKEMPreKey(2424, Curve.generateKeyPair());
|
||||
private final KEMSignedPreKey SAMPLE_PQ_KEY2 = KeysHelper.signedKEMPreKey(6868, Curve.generateKeyPair());
|
||||
private final KEMSignedPreKey SAMPLE_PQ_KEY3 = KeysHelper.signedKEMPreKey(1313, Curve.generateKeyPair());
|
||||
|
||||
private final KEMSignedPreKey SAMPLE_PQ_KEY_PNI = KeysHelper.signedKEMPreKey(8888, Curve.generateKeyPair());
|
||||
|
||||
private final ECSignedPreKey SAMPLE_SIGNED_KEY = KeysHelper.signedECPreKey(1111, IDENTITY_KEY_PAIR);
|
||||
private final ECSignedPreKey SAMPLE_SIGNED_KEY2 = KeysHelper.signedECPreKey(2222, IDENTITY_KEY_PAIR);
|
||||
private final ECSignedPreKey SAMPLE_SIGNED_KEY3 = KeysHelper.signedECPreKey(3333, IDENTITY_KEY_PAIR);
|
||||
private final ECSignedPreKey SAMPLE_SIGNED_PNI_KEY = KeysHelper.signedECPreKey(4444, PNI_IDENTITY_KEY_PAIR);
|
||||
private final ECSignedPreKey SAMPLE_SIGNED_PNI_KEY2 = KeysHelper.signedECPreKey(5555, PNI_IDENTITY_KEY_PAIR);
|
||||
private final ECSignedPreKey SAMPLE_SIGNED_PNI_KEY3 = KeysHelper.signedECPreKey(6666, PNI_IDENTITY_KEY_PAIR);
|
||||
private final ECSignedPreKey VALID_DEVICE_SIGNED_KEY = KeysHelper.signedECPreKey(89898, IDENTITY_KEY_PAIR);
|
||||
private final ECSignedPreKey VALID_DEVICE_PNI_SIGNED_KEY = KeysHelper.signedECPreKey(7777, PNI_IDENTITY_KEY_PAIR);
|
||||
|
||||
private final static KeysManager KEYS = mock(KeysManager.class );
|
||||
private final static AccountsManager accounts = mock(AccountsManager.class );
|
||||
private final static Account existsAccount = mock(Account.class );
|
||||
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private static final RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(
|
||||
AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new ServerRejectedExceptionMapper())
|
||||
.addResource(new KeysController(rateLimiters, KEYS, accounts))
|
||||
.addResource(new RateLimitExceededExceptionMapper())
|
||||
.build();
|
||||
|
||||
private Device sampleDevice;
|
||||
|
||||
private record WeaklyTypedPreKey(long keyId,
|
||||
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
byte[] publicKey) {
|
||||
}
|
||||
|
||||
private record WeaklyTypedSignedPreKey(long keyId,
|
||||
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
byte[] publicKey,
|
||||
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
byte[] signature) {
|
||||
|
||||
static WeaklyTypedSignedPreKey fromSignedPreKey(final SignedPreKey<?> signedPreKey) {
|
||||
return new WeaklyTypedSignedPreKey(signedPreKey.keyId(), signedPreKey.serializedPublicKey(), signedPreKey.signature());
|
||||
}
|
||||
}
|
||||
|
||||
private record WeaklyTypedPreKeyState(List<WeaklyTypedPreKey> preKeys,
|
||||
WeaklyTypedSignedPreKey signedPreKey,
|
||||
List<WeaklyTypedSignedPreKey> pqPreKeys,
|
||||
WeaklyTypedSignedPreKey pqLastResortPreKey,
|
||||
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
byte[] identityKey) {
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
sampleDevice = mock(Device.class);
|
||||
final Device sampleDevice2 = mock(Device.class);
|
||||
final Device sampleDevice3 = mock(Device.class);
|
||||
final Device sampleDevice4 = mock(Device.class);
|
||||
|
||||
final List<Device> allDevices = List.of(sampleDevice, sampleDevice2, sampleDevice3, sampleDevice4);
|
||||
|
||||
AccountsHelper.setupMockUpdate(accounts);
|
||||
|
||||
when(sampleDevice.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID);
|
||||
when(sampleDevice2.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2);
|
||||
when(sampleDevice3.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2);
|
||||
when(sampleDevice4.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID4);
|
||||
when(sampleDevice.getPhoneNumberIdentityRegistrationId()).thenReturn(OptionalInt.of(SAMPLE_PNI_REGISTRATION_ID));
|
||||
when(sampleDevice.isEnabled()).thenReturn(true);
|
||||
when(sampleDevice2.isEnabled()).thenReturn(true);
|
||||
when(sampleDevice3.isEnabled()).thenReturn(false);
|
||||
when(sampleDevice4.isEnabled()).thenReturn(true);
|
||||
when(sampleDevice.getSignedPreKey()).thenReturn(SAMPLE_SIGNED_KEY);
|
||||
when(sampleDevice2.getSignedPreKey()).thenReturn(SAMPLE_SIGNED_KEY2);
|
||||
when(sampleDevice3.getSignedPreKey()).thenReturn(SAMPLE_SIGNED_KEY3);
|
||||
when(sampleDevice4.getSignedPreKey()).thenReturn(null);
|
||||
when(sampleDevice.getPhoneNumberIdentitySignedPreKey()).thenReturn(SAMPLE_SIGNED_PNI_KEY);
|
||||
when(sampleDevice2.getPhoneNumberIdentitySignedPreKey()).thenReturn(SAMPLE_SIGNED_PNI_KEY2);
|
||||
when(sampleDevice3.getPhoneNumberIdentitySignedPreKey()).thenReturn(SAMPLE_SIGNED_PNI_KEY3);
|
||||
when(sampleDevice4.getPhoneNumberIdentitySignedPreKey()).thenReturn(null);
|
||||
when(sampleDevice.getId()).thenReturn(1L);
|
||||
when(sampleDevice2.getId()).thenReturn(2L);
|
||||
when(sampleDevice3.getId()).thenReturn(3L);
|
||||
when(sampleDevice4.getId()).thenReturn(4L);
|
||||
|
||||
when(existsAccount.getUuid()).thenReturn(EXISTS_UUID);
|
||||
when(existsAccount.getPhoneNumberIdentifier()).thenReturn(EXISTS_PNI);
|
||||
when(existsAccount.getDevice(1L)).thenReturn(Optional.of(sampleDevice));
|
||||
when(existsAccount.getDevice(2L)).thenReturn(Optional.of(sampleDevice2));
|
||||
when(existsAccount.getDevice(3L)).thenReturn(Optional.of(sampleDevice3));
|
||||
when(existsAccount.getDevice(4L)).thenReturn(Optional.of(sampleDevice4));
|
||||
when(existsAccount.getDevice(22L)).thenReturn(Optional.empty());
|
||||
when(existsAccount.getDevices()).thenReturn(allDevices);
|
||||
when(existsAccount.isEnabled()).thenReturn(true);
|
||||
when(existsAccount.getIdentityKey()).thenReturn(IDENTITY_KEY);
|
||||
when(existsAccount.getPhoneNumberIdentityKey()).thenReturn(PNI_IDENTITY_KEY);
|
||||
when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER);
|
||||
when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes()));
|
||||
|
||||
when(accounts.getByE164(EXISTS_NUMBER)).thenReturn(Optional.of(existsAccount));
|
||||
when(accounts.getByAccountIdentifier(EXISTS_UUID)).thenReturn(Optional.of(existsAccount));
|
||||
when(accounts.getByPhoneNumberIdentifier(EXISTS_PNI)).thenReturn(Optional.of(existsAccount));
|
||||
|
||||
when(accounts.getByE164(NOT_EXISTS_NUMBER)).thenReturn(Optional.empty());
|
||||
when(accounts.getByAccountIdentifier(NOT_EXISTS_UUID)).thenReturn(Optional.empty());
|
||||
|
||||
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
when(KEYS.store(any(), anyLong(), any(), any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(KEYS.getEcSignedPreKey(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||
when(KEYS.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
when(KEYS.takeEC(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY)));
|
||||
when(KEYS.takePQ(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY)));
|
||||
when(KEYS.takeEC(EXISTS_PNI, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY_PNI)));
|
||||
when(KEYS.takePQ(EXISTS_PNI, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY_PNI)));
|
||||
|
||||
when(KEYS.getEcCount(AuthHelper.VALID_UUID, 1)).thenReturn(CompletableFuture.completedFuture(5));
|
||||
when(KEYS.getPqCount(AuthHelper.VALID_UUID, 1)).thenReturn(CompletableFuture.completedFuture(5));
|
||||
|
||||
when(AuthHelper.VALID_DEVICE.getSignedPreKey()).thenReturn(VALID_DEVICE_SIGNED_KEY);
|
||||
when(AuthHelper.VALID_DEVICE.getPhoneNumberIdentitySignedPreKey()).thenReturn(VALID_DEVICE_PNI_SIGNED_KEY);
|
||||
when(AuthHelper.VALID_ACCOUNT.getIdentityKey()).thenReturn(null);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
reset(
|
||||
KEYS,
|
||||
accounts,
|
||||
existsAccount,
|
||||
rateLimiters,
|
||||
rateLimiter
|
||||
);
|
||||
|
||||
clearInvocations(AuthHelper.VALID_DEVICE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validKeyStatusTest() {
|
||||
PreKeyCount result = resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization",
|
||||
AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyCount.class);
|
||||
|
||||
assertThat(result.getCount()).isEqualTo(5);
|
||||
assertThat(result.getPqCount()).isEqualTo(5);
|
||||
|
||||
verify(KEYS).getEcCount(AuthHelper.VALID_UUID, 1);
|
||||
verify(KEYS).getPqCount(AuthHelper.VALID_UUID, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putSignedPreKeyV2() {
|
||||
ECSignedPreKey test = KeysHelper.signedECPreKey(9998, IDENTITY_KEY_PAIR);
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v2/keys/signed")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(test, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(test));
|
||||
verify(AuthHelper.VALID_DEVICE, never()).setPhoneNumberIdentitySignedPreKey(any());
|
||||
verify(accounts).updateDevice(eq(AuthHelper.VALID_ACCOUNT), anyLong(), any());
|
||||
verify(KEYS).storeEcSignedPreKeys(AuthHelper.VALID_UUID, Map.of(Device.MASTER_ID, test));
|
||||
}
|
||||
|
||||
@Test
|
||||
void putPhoneNumberIdentitySignedPreKeyV2() {
|
||||
final ECSignedPreKey replacementKey = KeysHelper.signedECPreKey(9998, PNI_IDENTITY_KEY_PAIR);
|
||||
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v2/keys/signed")
|
||||
.queryParam("identity", "pni")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(replacementKey, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
verify(AuthHelper.VALID_DEVICE).setPhoneNumberIdentitySignedPreKey(eq(replacementKey));
|
||||
verify(AuthHelper.VALID_DEVICE, never()).setSignedPreKey(any());
|
||||
verify(accounts).updateDevice(eq(AuthHelper.VALID_ACCOUNT), anyLong(), any());
|
||||
verify(KEYS).storeEcSignedPreKeys(AuthHelper.VALID_PNI, Map.of(Device.MASTER_ID, replacementKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledPutSignedPreKeyV2() {
|
||||
ECSignedPreKey test = KeysHelper.signedECPreKey(9999, IDENTITY_KEY_PAIR);
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v2/keys/signed")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD))
|
||||
.put(Entity.entity(test, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validSingleRequestTestV2() {
|
||||
PreKeyResponse result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_UUID))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertEquals(SAMPLE_KEY, result.getDevice(1).getPreKey());
|
||||
assertThat(result.getDevice(1).getPqPreKey()).isNull();
|
||||
assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID);
|
||||
assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(), result.getDevice(1).getSignedPreKey());
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validSingleRequestPqTestNoPqKeysV2() {
|
||||
when(KEYS.takePQ(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||
|
||||
PreKeyResponse result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_UUID))
|
||||
.queryParam("pq", "true")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertEquals(SAMPLE_KEY, result.getDevice(1).getPreKey());
|
||||
assertThat(result.getDevice(1).getPqPreKey()).isNull();
|
||||
assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID);
|
||||
assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(), result.getDevice(1).getSignedPreKey());
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 1);
|
||||
verify(KEYS).takePQ(EXISTS_UUID, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validSingleRequestPqTestV2() {
|
||||
PreKeyResponse result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_UUID))
|
||||
.queryParam("pq", "true")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertEquals(SAMPLE_KEY, result.getDevice(1).getPreKey());
|
||||
assertEquals(SAMPLE_PQ_KEY, result.getDevice(1).getPqPreKey());
|
||||
assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID);
|
||||
assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(), result.getDevice(1).getSignedPreKey());
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 1);
|
||||
verify(KEYS).takePQ(EXISTS_UUID, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validSingleRequestByPhoneNumberIdentifierTestV2() {
|
||||
PreKeyResponse result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_PNI))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getPhoneNumberIdentityKey());
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertEquals(SAMPLE_KEY_PNI, result.getDevice(1).getPreKey());
|
||||
assertThat(result.getDevice(1).getPqPreKey()).isNull();
|
||||
assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_PNI_REGISTRATION_ID);
|
||||
assertEquals(existsAccount.getDevice(1).get().getPhoneNumberIdentitySignedPreKey(), result.getDevice(1).getSignedPreKey());
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_PNI, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_PNI, 1);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validSingleRequestPqByPhoneNumberIdentifierTestV2() {
|
||||
PreKeyResponse result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_PNI))
|
||||
.queryParam("pq", "true")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getPhoneNumberIdentityKey());
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertEquals(SAMPLE_KEY_PNI, result.getDevice(1).getPreKey());
|
||||
assertThat(result.getDevice(1).getPqPreKey()).isEqualTo(SAMPLE_PQ_KEY_PNI);
|
||||
assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_PNI_REGISTRATION_ID);
|
||||
assertEquals(existsAccount.getDevice(1).get().getPhoneNumberIdentitySignedPreKey(), result.getDevice(1).getSignedPreKey());
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_PNI, 1);
|
||||
verify(KEYS).takePQ(EXISTS_PNI, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_PNI, 1);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validSingleRequestByPhoneNumberIdentifierNoPniRegistrationIdTestV2() {
|
||||
when(sampleDevice.getPhoneNumberIdentityRegistrationId()).thenReturn(OptionalInt.empty());
|
||||
|
||||
PreKeyResponse result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_PNI))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getPhoneNumberIdentityKey());
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertEquals(SAMPLE_KEY_PNI, result.getDevice(1).getPreKey());
|
||||
assertThat(result.getDevice(1).getPqPreKey()).isNull();
|
||||
assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID);
|
||||
assertEquals(existsAccount.getDevice(1).get().getPhoneNumberIdentitySignedPreKey(), result.getDevice(1).getSignedPreKey());
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_PNI, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_PNI, 1);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetKeysRateLimited() throws RateLimitExceededException {
|
||||
Duration retryAfter = Duration.ofSeconds(31);
|
||||
doThrow(new RateLimitExceededException(retryAfter, true)).when(rateLimiter).validate(anyString());
|
||||
|
||||
Response result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/*", EXISTS_PNI))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(result.getStatus()).isEqualTo(413);
|
||||
assertThat(result.getHeaderString("Retry-After")).isEqualTo(String.valueOf(retryAfter.toSeconds()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnidentifiedRequest() {
|
||||
PreKeyResponse result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_UUID))
|
||||
.queryParam("pq", "true")
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes()))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertEquals(SAMPLE_KEY, result.getDevice(1).getPreKey());
|
||||
assertEquals(SAMPLE_PQ_KEY, result.getDevice(1).getPqPreKey());
|
||||
assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(), result.getDevice(1).getSignedPreKey());
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 1);
|
||||
verify(KEYS).takePQ(EXISTS_UUID, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoDevices() {
|
||||
|
||||
when(existsAccount.getDevices()).thenReturn(Collections.emptyList());
|
||||
|
||||
Response result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/*", EXISTS_UUID))
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes()))
|
||||
.get();
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnauthorizedUnidentifiedRequest() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_UUID))
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("9999".getBytes()))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMalformedUnidentifiedRequest() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_UUID))
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, "$$$$$$$$$")
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void validMultiRequestTestV2() {
|
||||
when(KEYS.takeEC(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY)));
|
||||
when(KEYS.takeEC(EXISTS_UUID, 2)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY2)));
|
||||
when(KEYS.takeEC(EXISTS_UUID, 3)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY3)));
|
||||
when(KEYS.takeEC(EXISTS_UUID, 4)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY4)));
|
||||
|
||||
PreKeyResponse results = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/*", EXISTS_UUID))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(results.getDevicesCount()).isEqualTo(3);
|
||||
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||
|
||||
ECSignedPreKey signedPreKey = results.getDevice(1).getSignedPreKey();
|
||||
ECPreKey preKey = results.getDevice(1).getPreKey();
|
||||
long registrationId = results.getDevice(1).getRegistrationId();
|
||||
long deviceId = results.getDevice(1).getDeviceId();
|
||||
|
||||
assertEquals(SAMPLE_KEY, preKey);
|
||||
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID);
|
||||
assertEquals(SAMPLE_SIGNED_KEY, signedPreKey);
|
||||
assertThat(deviceId).isEqualTo(1);
|
||||
|
||||
signedPreKey = results.getDevice(2).getSignedPreKey();
|
||||
preKey = results.getDevice(2).getPreKey();
|
||||
registrationId = results.getDevice(2).getRegistrationId();
|
||||
deviceId = results.getDevice(2).getDeviceId();
|
||||
|
||||
assertEquals(SAMPLE_KEY2, preKey);
|
||||
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID2);
|
||||
assertEquals(SAMPLE_SIGNED_KEY2, signedPreKey);
|
||||
assertThat(deviceId).isEqualTo(2);
|
||||
|
||||
signedPreKey = results.getDevice(4).getSignedPreKey();
|
||||
preKey = results.getDevice(4).getPreKey();
|
||||
registrationId = results.getDevice(4).getRegistrationId();
|
||||
deviceId = results.getDevice(4).getDeviceId();
|
||||
|
||||
assertEquals(SAMPLE_KEY4, preKey);
|
||||
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID4);
|
||||
assertThat(signedPreKey).isNull();
|
||||
assertThat(deviceId).isEqualTo(4);
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 1);
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 2);
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 4);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 2);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 4);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validMultiRequestPqTestV2() {
|
||||
when(KEYS.takeEC(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||
when(KEYS.takePQ(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||
|
||||
when(KEYS.takeEC(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY)));
|
||||
when(KEYS.takeEC(EXISTS_UUID, 3)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY3)));
|
||||
when(KEYS.takeEC(EXISTS_UUID, 4)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY4)));
|
||||
when(KEYS.takePQ(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY)));
|
||||
when(KEYS.takePQ(EXISTS_UUID, 2)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY2)));
|
||||
when(KEYS.takePQ(EXISTS_UUID, 3)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY3)));
|
||||
|
||||
PreKeyResponse results = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/*", EXISTS_UUID))
|
||||
.queryParam("pq", "true")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(results.getDevicesCount()).isEqualTo(3);
|
||||
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||
|
||||
ECSignedPreKey signedPreKey = results.getDevice(1).getSignedPreKey();
|
||||
ECPreKey preKey = results.getDevice(1).getPreKey();
|
||||
KEMSignedPreKey pqPreKey = results.getDevice(1).getPqPreKey();
|
||||
long registrationId = results.getDevice(1).getRegistrationId();
|
||||
long deviceId = results.getDevice(1).getDeviceId();
|
||||
|
||||
assertEquals(SAMPLE_KEY, preKey);
|
||||
assertEquals(SAMPLE_PQ_KEY, pqPreKey);
|
||||
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID);
|
||||
assertEquals(SAMPLE_SIGNED_KEY, signedPreKey);
|
||||
assertThat(deviceId).isEqualTo(1);
|
||||
|
||||
signedPreKey = results.getDevice(2).getSignedPreKey();
|
||||
preKey = results.getDevice(2).getPreKey();
|
||||
pqPreKey = results.getDevice(2).getPqPreKey();
|
||||
registrationId = results.getDevice(2).getRegistrationId();
|
||||
deviceId = results.getDevice(2).getDeviceId();
|
||||
|
||||
assertThat(preKey).isNull();
|
||||
assertEquals(SAMPLE_PQ_KEY2, pqPreKey);
|
||||
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID2);
|
||||
assertEquals(SAMPLE_SIGNED_KEY2, signedPreKey);
|
||||
assertThat(deviceId).isEqualTo(2);
|
||||
|
||||
signedPreKey = results.getDevice(4).getSignedPreKey();
|
||||
preKey = results.getDevice(4).getPreKey();
|
||||
pqPreKey = results.getDevice(4).getPqPreKey();
|
||||
registrationId = results.getDevice(4).getRegistrationId();
|
||||
deviceId = results.getDevice(4).getDeviceId();
|
||||
|
||||
assertEquals(SAMPLE_KEY4, preKey);
|
||||
assertThat(pqPreKey).isNull();
|
||||
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID4);
|
||||
assertThat(signedPreKey).isNull();
|
||||
assertThat(deviceId).isEqualTo(4);
|
||||
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 1);
|
||||
verify(KEYS).takePQ(EXISTS_UUID, 1);
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 2);
|
||||
verify(KEYS).takePQ(EXISTS_UUID, 2);
|
||||
verify(KEYS).takeEC(EXISTS_UUID, 4);
|
||||
verify(KEYS).takePQ(EXISTS_UUID, 4);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 2);
|
||||
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 4);
|
||||
verifyNoMoreInteractions(KEYS);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void invalidRequestTestV2() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s", NOT_EXISTS_UUID))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void anotherInvalidRequestTestV2() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/22", EXISTS_UUID))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unauthorizedRequestTestV2() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_UUID))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
|
||||
|
||||
response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_UUID))
|
||||
.request()
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putKeysTestV2() {
|
||||
final ECPreKey preKey = KeysHelper.ecPreKey(31337);
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair);
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
|
||||
PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<List<ECPreKey>> listCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(KEYS).store(eq(AuthHelper.VALID_UUID), eq(1L), listCaptor.capture(), isNull(), eq(signedPreKey), isNull());
|
||||
|
||||
assertThat(listCaptor.getValue()).containsExactly(preKey);
|
||||
|
||||
verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq(identityKey));
|
||||
verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(signedPreKey));
|
||||
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void putKeysPqTestV2() {
|
||||
final ECPreKey preKey = KeysHelper.ecPreKey(31337);
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair);
|
||||
final KEMSignedPreKey pqPreKey = KeysHelper.signedKEMPreKey(31339, identityKeyPair);
|
||||
final KEMSignedPreKey pqLastResortPreKey = KeysHelper.signedKEMPreKey(31340, identityKeyPair);
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
|
||||
PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey), List.of(pqPreKey), pqLastResortPreKey);
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<List<ECPreKey>> ecCaptor = ArgumentCaptor.forClass(List.class);
|
||||
ArgumentCaptor<List<KEMSignedPreKey>> pqCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(KEYS).store(eq(AuthHelper.VALID_UUID), eq(1L), ecCaptor.capture(), pqCaptor.capture(), eq(signedPreKey), eq(pqLastResortPreKey));
|
||||
|
||||
assertThat(ecCaptor.getValue()).containsExactly(preKey);
|
||||
assertThat(pqCaptor.getValue()).containsExactly(pqPreKey);
|
||||
|
||||
verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq(identityKey));
|
||||
verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(signedPreKey));
|
||||
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void putKeysStructurallyInvalidSignedECKey() {
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
final KEMSignedPreKey wrongPreKey = KeysHelper.signedKEMPreKey(1, identityKeyPair);
|
||||
final WeaklyTypedPreKeyState preKeyState =
|
||||
new WeaklyTypedPreKeyState(null, WeaklyTypedSignedPreKey.fromSignedPreKey(wrongPreKey), null, null, identityKey.serialize());
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putKeysStructurallyInvalidUnsignedECKey() {
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
final WeaklyTypedPreKey wrongPreKey = new WeaklyTypedPreKey(1, "cluck cluck i'm a parrot".getBytes());
|
||||
final WeaklyTypedPreKeyState preKeyState =
|
||||
new WeaklyTypedPreKeyState(List.of(wrongPreKey), null, null, null, identityKey.serialize());
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putKeysStructurallyInvalidPQOneTimeKey() {
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
final WeaklyTypedSignedPreKey wrongPreKey = WeaklyTypedSignedPreKey.fromSignedPreKey(KeysHelper.signedECPreKey(1, identityKeyPair));
|
||||
final WeaklyTypedPreKeyState preKeyState =
|
||||
new WeaklyTypedPreKeyState(null, null, List.of(wrongPreKey), null, identityKey.serialize());
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putKeysStructurallyInvalidPQLastResortKey() {
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
final WeaklyTypedSignedPreKey wrongPreKey = WeaklyTypedSignedPreKey.fromSignedPreKey(KeysHelper.signedECPreKey(1, identityKeyPair));
|
||||
final WeaklyTypedPreKeyState preKeyState =
|
||||
new WeaklyTypedPreKeyState(null, null, null, wrongPreKey, identityKey.serialize());
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putKeysByPhoneNumberIdentifierTestV2() {
|
||||
final ECPreKey preKey = KeysHelper.ecPreKey(31337);
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair);
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
|
||||
PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.queryParam("identity", "pni")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<List<ECPreKey>> listCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(KEYS).store(eq(AuthHelper.VALID_PNI), eq(1L), listCaptor.capture(), isNull(), eq(signedPreKey), isNull());
|
||||
|
||||
assertThat(listCaptor.getValue()).containsExactly(preKey);
|
||||
|
||||
verify(AuthHelper.VALID_ACCOUNT).setPhoneNumberIdentityKey(eq(identityKey));
|
||||
verify(AuthHelper.VALID_DEVICE).setPhoneNumberIdentitySignedPreKey(eq(signedPreKey));
|
||||
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void putKeysByPhoneNumberIdentifierPqTestV2() {
|
||||
final ECPreKey preKey = KeysHelper.ecPreKey(31337);
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair);
|
||||
final KEMSignedPreKey pqPreKey = KeysHelper.signedKEMPreKey(31339, identityKeyPair);
|
||||
final KEMSignedPreKey pqLastResortPreKey = KeysHelper.signedKEMPreKey(31340, identityKeyPair);
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
|
||||
PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey), List.of(pqPreKey), pqLastResortPreKey);
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.queryParam("identity", "pni")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<List<ECPreKey>> ecCaptor = ArgumentCaptor.forClass(List.class);
|
||||
ArgumentCaptor<List<KEMSignedPreKey>> pqCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(KEYS).store(eq(AuthHelper.VALID_PNI), eq(1L), ecCaptor.capture(), pqCaptor.capture(), eq(signedPreKey), eq(pqLastResortPreKey));
|
||||
|
||||
assertThat(ecCaptor.getValue()).containsExactly(preKey);
|
||||
assertThat(pqCaptor.getValue()).containsExactly(pqPreKey);
|
||||
|
||||
verify(AuthHelper.VALID_ACCOUNT).setPhoneNumberIdentityKey(eq(identityKey));
|
||||
verify(AuthHelper.VALID_DEVICE).setPhoneNumberIdentitySignedPreKey(eq(signedPreKey));
|
||||
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void putPrekeyWithInvalidSignature() {
|
||||
final ECSignedPreKey badSignedPreKey = KeysHelper.signedECPreKey(1, Curve.generateKeyPair());
|
||||
PreKeyState preKeyState = new PreKeyState(IDENTITY_KEY, badSignedPreKey, List.of());
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.queryParam("identity", "aci")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledPutKeysTestV2() {
|
||||
final ECPreKey preKey = KeysHelper.ecPreKey(31337);
|
||||
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||
final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair);
|
||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
||||
|
||||
PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<List<ECPreKey>> listCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(KEYS).store(eq(AuthHelper.DISABLED_UUID), eq(1L), listCaptor.capture(), isNull(), eq(signedPreKey), isNull());
|
||||
|
||||
List<ECPreKey> capturedList = listCaptor.getValue();
|
||||
assertThat(capturedList.size()).isEqualTo(1);
|
||||
assertThat(capturedList.get(0).keyId()).isEqualTo(31337);
|
||||
assertThat(capturedList.get(0).publicKey()).isEqualTo(preKey.publicKey());
|
||||
|
||||
verify(AuthHelper.DISABLED_ACCOUNT).setIdentityKey(eq(identityKey));
|
||||
verify(AuthHelper.DISABLED_DEVICE).setSignedPreKey(eq(signedPreKey));
|
||||
verify(accounts).update(eq(AuthHelper.DISABLED_ACCOUNT), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void putIdentityKeyNonPrimary() {
|
||||
final ECPreKey preKey = KeysHelper.ecPreKey(31337);
|
||||
final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, IDENTITY_KEY_PAIR);
|
||||
|
||||
List<ECPreKey> preKeys = List.of(preKey);
|
||||
|
||||
PreKeyState preKeyState = new PreKeyState(IDENTITY_KEY, signedPreKey, preKeys);
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/keys")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_3, 2L, AuthHelper.VALID_PASSWORD_3_LINKED))
|
||||
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||
import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class PaymentsControllerTest {
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator paymentsCredentialsGenerator = mock(ExternalServiceCredentialsGenerator.class);
|
||||
private static final CurrencyConversionManager currencyManager = mock(CurrencyConversionManager.class);
|
||||
|
||||
private final ExternalServiceCredentials validCredentials = new ExternalServiceCredentials("username", "password");
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new PaymentsController(currencyManager, paymentsCredentialsGenerator))
|
||||
.build();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
when(paymentsCredentialsGenerator.generateForUuid(eq(AuthHelper.VALID_UUID))).thenReturn(validCredentials);
|
||||
when(currencyManager.getCurrencyConversions()).thenReturn(Optional.of(
|
||||
new CurrencyConversionEntityList(List.of(
|
||||
new CurrencyConversionEntity("FOO", Map.of(
|
||||
"USD", new BigDecimal("2.35"),
|
||||
"EUR", new BigDecimal("1.89")
|
||||
)),
|
||||
new CurrencyConversionEntity("BAR", Map.of(
|
||||
"USD", new BigDecimal("1.50"),
|
||||
"EUR", new BigDecimal("0.98")
|
||||
))
|
||||
), System.currentTimeMillis())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAuthToken() {
|
||||
ExternalServiceCredentials token =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/payments/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(ExternalServiceCredentials.class);
|
||||
|
||||
assertThat(token.username()).isEqualTo(validCredentials.username());
|
||||
assertThat(token.password()).isEqualTo(validCredentials.password());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidAuthGetAuthToken() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/payments/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisabledGetAuthToken() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/payments/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD))
|
||||
.get();
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCurrencyConversions() {
|
||||
CurrencyConversionEntityList conversions =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/payments/conversions")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(CurrencyConversionEntityList.class);
|
||||
|
||||
|
||||
assertThat(conversions.getCurrencies().size()).isEqualTo(2);
|
||||
assertThat(conversions.getCurrencies().get(0).getBase()).isEqualTo("FOO");
|
||||
assertThat(conversions.getCurrencies().get(0).getConversions().get("USD")).isEqualTo(new BigDecimal("2.35"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCurrencyConversions_Json() {
|
||||
String json =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/payments/conversions")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(String.class);
|
||||
|
||||
// the currency serialization might occur in either order
|
||||
assertThat(json).containsPattern("\\{(\"EUR\":1.89,\"USD\":2.35|\"USD\":2.35,\"EUR\":1.89)}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.signal.event.NoOpAdminEventLogger;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class RemoteConfigControllerTest {
|
||||
|
||||
private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class);
|
||||
|
||||
private static final Set<String> remoteConfigsUsers = Set.of("user1@example.com", "user2@example.com");
|
||||
|
||||
private static final String requiredHostedDomain = "example.com";
|
||||
private static final GoogleIdTokenVerifier.Builder googleIdVerificationTokenBuilder = mock(
|
||||
GoogleIdTokenVerifier.Builder.class);
|
||||
private static final GoogleIdTokenVerifier googleIdTokenVerifier = mock(GoogleIdTokenVerifier.class);
|
||||
|
||||
static {
|
||||
when(googleIdVerificationTokenBuilder.setAudience(any())).thenReturn(googleIdVerificationTokenBuilder);
|
||||
when(googleIdVerificationTokenBuilder.build()).thenReturn(googleIdTokenVerifier);
|
||||
}
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addProvider(new DeviceLimitExceededExceptionMapper())
|
||||
.addResource(new RemoteConfigController(remoteConfigsManager, new NoOpAdminEventLogger(), remoteConfigsUsers,
|
||||
requiredHostedDomain, Collections.singletonList("aud.example.com"),
|
||||
googleIdVerificationTokenBuilder, Map.of("maxGroupSize", "42")))
|
||||
.build();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
|
||||
add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.DISABLED_UUID, AuthHelper.INVALID_UUID), null,
|
||||
null, null));
|
||||
add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("always.true", 100, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null));
|
||||
add(new RemoteConfig("value.always.true", 100, Set.of(), "foo", "bar", null));
|
||||
add(new RemoteConfig("value.only.special", 0, Set.of(AuthHelper.VALID_UUID), "abc", "xyz", null));
|
||||
add(new RemoteConfig("value.always.false", 0, Set.of(), "red", "green", null));
|
||||
add(new RemoteConfig("linked.config.0", 50, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0"));
|
||||
add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null));
|
||||
}});
|
||||
|
||||
final Map<String, GoogleIdToken> googleIdTokens = new HashMap<>();
|
||||
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
final String user = "user" + i;
|
||||
final GoogleIdToken googleIdToken = mock(GoogleIdToken.class);
|
||||
final GoogleIdToken.Payload payload = mock(GoogleIdToken.Payload.class);
|
||||
when(googleIdToken.getPayload()).thenReturn(payload);
|
||||
|
||||
when(payload.getEmail()).thenReturn(user + "@" + requiredHostedDomain);
|
||||
when(payload.getEmailVerified()).thenReturn(true);
|
||||
when(payload.getHostedDomain()).thenReturn(requiredHostedDomain);
|
||||
|
||||
googleIdTokens.put(user + ".valid", googleIdToken);
|
||||
}
|
||||
|
||||
when(googleIdTokenVerifier.verify(anyString()))
|
||||
.thenAnswer(answer -> googleIdTokens.get(answer.getArgument(0, String.class)));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
reset(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfig() {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(UserRemoteConfigList.class);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).getAll();
|
||||
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
|
||||
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
|
||||
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
|
||||
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
|
||||
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
|
||||
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
|
||||
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
|
||||
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("xyz");
|
||||
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
|
||||
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
|
||||
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
|
||||
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
|
||||
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
|
||||
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfigNotSpecial() {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
|
||||
.get(UserRemoteConfigList.class);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).getAll();
|
||||
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
|
||||
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
|
||||
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
|
||||
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
|
||||
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
|
||||
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
|
||||
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
|
||||
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("abc");
|
||||
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
|
||||
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
|
||||
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
|
||||
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
|
||||
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
|
||||
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHashKeyLinkedConfigs() {
|
||||
boolean allUnlinkedConfigsMatched = true;
|
||||
for (AuthHelper.TestAccount testAccount : AuthHelper.TEST_ACCOUNTS) {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest().target("/v1/config/").request().header("Authorization", testAccount.getAuthHeader()).get(UserRemoteConfigList.class);
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
|
||||
final UserRemoteConfig linkedConfig0 = configuration.getConfig().get(7);
|
||||
assertThat(linkedConfig0.getName()).isEqualTo("linked.config.0");
|
||||
|
||||
final UserRemoteConfig linkedConfig1 = configuration.getConfig().get(8);
|
||||
assertThat(linkedConfig1.getName()).isEqualTo("linked.config.1");
|
||||
|
||||
final UserRemoteConfig unlinkedConfig = configuration.getConfig().get(9);
|
||||
assertThat(unlinkedConfig.getName()).isEqualTo("unlinked.config");
|
||||
|
||||
assertThat(linkedConfig0.isEnabled() == linkedConfig1.isEnabled()).isTrue();
|
||||
allUnlinkedConfigsMatched &= (linkedConfig0.isEnabled() == unlinkedConfig.isEnabled());
|
||||
}
|
||||
assertThat(allUnlinkedConfigsMatched).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfigUnauthorized() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetConfig() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.header("Config-Token", "user1.valid")
|
||||
.put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<RemoteConfig> captor = ArgumentCaptor.forClass(RemoteConfig.class);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).set(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getName()).isEqualTo("android.stickers");
|
||||
assertThat(captor.getValue().getPercentage()).isEqualTo(88);
|
||||
assertThat(captor.getValue().getUuids()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetConfigValued() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.header("Config-Token", "user1.valid")
|
||||
.put(Entity.entity(new RemoteConfig("value.sometimes", 50, Set.of(), "a", "b", null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<RemoteConfig> captor = ArgumentCaptor.forClass(RemoteConfig.class);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).set(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getName()).isEqualTo("value.sometimes");
|
||||
assertThat(captor.getValue().getPercentage()).isEqualTo(50);
|
||||
assertThat(captor.getValue().getUuids()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetConfigWithHashKey() {
|
||||
final String configToken = "user1.valid";
|
||||
Response response1 = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.header("Config-Token", configToken)
|
||||
.put(Entity.entity(new RemoteConfig("linked.config.0", 50, Set.of(), "FALSE", "TRUE", null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response1.getStatus()).isEqualTo(204);
|
||||
|
||||
Response response2 = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.header("Config-Token", configToken)
|
||||
.put(Entity.entity(new RemoteConfig("linked.config.1", 50, Set.of(), "FALSE", "TRUE", "linked.config.0"), MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response2.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<RemoteConfig> captor = ArgumentCaptor.forClass(RemoteConfig.class);
|
||||
|
||||
verify(remoteConfigsManager, times(2)).set(captor.capture());
|
||||
assertThat(captor.getAllValues()).hasSize(2);
|
||||
|
||||
final RemoteConfig capture1 = captor.getAllValues().get(0);
|
||||
assertThat(capture1).isNotNull();
|
||||
assertThat(capture1.getName()).isEqualTo("linked.config.0");
|
||||
assertThat(capture1.getPercentage()).isEqualTo(50);
|
||||
assertThat(capture1.getUuids()).isEmpty();
|
||||
assertThat(capture1.getHashKey()).isNull();
|
||||
|
||||
final RemoteConfig capture2 = captor.getAllValues().get(1);
|
||||
assertThat(capture2).isNotNull();
|
||||
assertThat(capture2.getName()).isEqualTo("linked.config.1");
|
||||
assertThat(capture2.getPercentage()).isEqualTo(50);
|
||||
assertThat(capture2.getUuids()).isEmpty();
|
||||
assertThat(capture2.getHashKey()).isEqualTo("linked.config.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetConfigUnauthorized() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.header("Config-Token", "user3.valid")
|
||||
.put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetConfigMissingUnauthorized() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetConfigBadName() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.header("Config-Token", "user1.valid")
|
||||
.put(Entity.entity(new RemoteConfig("android-stickers", 88, Set.of(), "FALSE", "TRUE", null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetConfigEmptyName() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.header("Config-Token", "user1.valid")
|
||||
.put(Entity.entity(new RemoteConfig("", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetGlobalConfig() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config")
|
||||
.request()
|
||||
.header("Config-Token", "user1.valid")
|
||||
.put(Entity.entity(new RemoteConfig("global.maxGroupSize", 88, Set.of(), "FALSE", "TRUE", null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDelete() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config/android.stickers")
|
||||
.request()
|
||||
.header("Config-Token", "user1.valid")
|
||||
.delete();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).delete("android.stickers");
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUnauthorized() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config/android.stickers")
|
||||
.request()
|
||||
.header("Config-Token", "baz")
|
||||
.delete();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteGlobalConfig() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config/global.maxGroupSize")
|
||||
.request()
|
||||
.header("Config-Token", "user1.valid")
|
||||
.delete();
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMath() throws NoSuchAlgorithmException {
|
||||
List<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();
|
||||
Map<String, Integer> enabledMap = new HashMap<>();
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||
int iterations = 100000;
|
||||
Random random = new Random(9424242L); // the seed value doesn't matter so much as it's constant to make the test not flaky
|
||||
|
||||
for (int i=0;i<iterations;i++) {
|
||||
for (RemoteConfig config : remoteConfigList) {
|
||||
int count = enabledMap.getOrDefault(config.getName(), 0);
|
||||
|
||||
if (RemoteConfigController.isInBucket(digest, AuthHelper.getRandomUUID(random), config.getName().getBytes(), config.getPercentage(), new HashSet<>())) {
|
||||
count++;
|
||||
}
|
||||
|
||||
enabledMap.put(config.getName(), count);
|
||||
}
|
||||
}
|
||||
|
||||
for (RemoteConfig config : remoteConfigList) {
|
||||
double targetNumber = iterations * (config.getPercentage() / 100.0);
|
||||
double variance = targetNumber * 0.01;
|
||||
|
||||
assertThat(enabledMap.get(config.getName())).isBetween((int) (targetNumber - variance),
|
||||
(int) (targetNumber + variance));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class SecureStorageControllerTest {
|
||||
|
||||
private static final SecureStorageServiceConfiguration STORAGE_CFG = MockUtils.buildMock(
|
||||
SecureStorageServiceConfiguration.class,
|
||||
cfg -> when(cfg.userAuthenticationTokenSharedSecret()).thenReturn(randomSecretBytes(32)));
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator STORAGE_CREDENTIAL_GENERATOR = SecureStorageController
|
||||
.credentialsGenerator(STORAGE_CFG);
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new SecureStorageController(STORAGE_CREDENTIAL_GENERATOR))
|
||||
.build();
|
||||
|
||||
|
||||
@Test
|
||||
void testGetCredentials() throws Exception {
|
||||
ExternalServiceCredentials credentials = resources.getJerseyTest()
|
||||
.target("/v1/storage/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(ExternalServiceCredentials.class);
|
||||
|
||||
assertThat(credentials.password()).isNotEmpty();
|
||||
assertThat(credentials.username()).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCredentialsBadAuth() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/storage/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.util.Base64;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class StickerControllerTest {
|
||||
|
||||
private static final RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new StickerController(rateLimiters, "foo", "bar", "us-east-1", "mybucket"))
|
||||
.build();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
when(rateLimiters.getStickerPackLimiter()).thenReturn(rateLimiter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreatePack() throws RateLimitExceededException {
|
||||
StickerPackFormUploadAttributes attributes = resources.getJerseyTest()
|
||||
.target("/v1/sticker/pack/form/10")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(StickerPackFormUploadAttributes.class);
|
||||
|
||||
assertThat(attributes.getPackId()).isNotNull();
|
||||
assertThat(attributes.getPackId().length()).isEqualTo(32);
|
||||
|
||||
assertThat(attributes.getManifest()).isNotNull();
|
||||
assertThat(attributes.getManifest().getKey()).isEqualTo("stickers/" + attributes.getPackId() + "/manifest.proto");
|
||||
assertThat(attributes.getManifest().getAcl()).isEqualTo("private");
|
||||
assertThat(attributes.getManifest().getPolicy()).isNotEmpty();
|
||||
assertThat(new String(Base64.getDecoder().decode(attributes.getManifest().getPolicy()))).contains("[\"content-length-range\", 1, 10240]");
|
||||
assertThat(attributes.getManifest().getSignature()).isNotEmpty();
|
||||
assertThat(attributes.getManifest().getAlgorithm()).isEqualTo("AWS4-HMAC-SHA256");
|
||||
assertThat(attributes.getManifest().getCredential()).isNotEmpty();
|
||||
assertThat(attributes.getManifest().getId()).isEqualTo(-1);
|
||||
|
||||
assertThat(attributes.getStickers().size()).isEqualTo(10);
|
||||
|
||||
for (int i=0;i<10;i++) {
|
||||
assertThat(attributes.getStickers().get(i).getId()).isEqualTo(i);
|
||||
assertThat(attributes.getStickers().get(i).getKey()).isEqualTo("stickers/" + attributes.getPackId() + "/full/" + i);
|
||||
assertThat(attributes.getStickers().get(i).getAcl()).isEqualTo("private");
|
||||
assertThat(attributes.getStickers().get(i).getPolicy()).isNotEmpty();
|
||||
assertThat(new String(Base64.getDecoder().decode(attributes.getStickers().get(i).getPolicy()))).contains("[\"content-length-range\", 1, 308224]");
|
||||
assertThat(attributes.getStickers().get(i).getSignature()).isNotEmpty();
|
||||
assertThat(attributes.getStickers().get(i).getAlgorithm()).isEqualTo("AWS4-HMAC-SHA256");
|
||||
assertThat(attributes.getStickers().get(i).getCredential()).isNotEmpty();
|
||||
}
|
||||
|
||||
verify(rateLimiters, times(1)).getStickerPackLimiter();
|
||||
verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateTooLargePack() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/sticker/pack/form/202")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user