test classes moved to same packages with components they test

This commit is contained in:
Sergey Skrobotov
2023-07-17 12:58:10 -07:00
parent b8d8d349f4
commit 352e1b2249
42 changed files with 84 additions and 136 deletions

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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))
);
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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)}");
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}