mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-22 09:28:30 +01:00
Add receipt redemption API to chat server
This commit is contained in:
@@ -19,6 +19,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ListResourceBundle;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.ResourceBundle.Control;
|
||||
import java.util.stream.Stream;
|
||||
@@ -80,7 +81,7 @@ public class ConfiguredProfileBadgeConverterTest {
|
||||
return objects;
|
||||
}
|
||||
};
|
||||
return new BadgesConfiguration(badges, List.of());
|
||||
return new BadgesConfiguration(badges, List.of(), Map.of());
|
||||
}
|
||||
|
||||
private BadgeConfiguration getBadge(BadgesConfiguration badgesConfiguration, int i) {
|
||||
|
||||
@@ -10,70 +10,151 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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.github.tomakehurst.wiremock.junit.WireMockRule;
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
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.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
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.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.signal.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.configuration.CircuitBreakerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.DonationController;
|
||||
import org.whispersystems.textsecuregcm.entities.ApplePayAuthorizationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ApplePayAuthorizationResponse;
|
||||
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.SystemMapper;
|
||||
|
||||
public class DonationControllerTest {
|
||||
|
||||
private static final Executor executor = Executors.newSingleThreadExecutor();
|
||||
private static final Executor httpClientExecutor = Executors.newSingleThreadExecutor();
|
||||
private static final long nowEpochSeconds = 1_500_000_000L;
|
||||
|
||||
@Rule
|
||||
public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort().dynamicHttpsPort());
|
||||
@RegisterExtension
|
||||
static WireMockExtension wm = WireMockExtension.newInstance()
|
||||
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
|
||||
.build();
|
||||
static SecureRandom secureRandom;
|
||||
static {
|
||||
try {
|
||||
secureRandom = SecureRandom.getInstanceStrong();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceExtension resources;
|
||||
|
||||
@Before
|
||||
public void before() throws Throwable {
|
||||
static DonationConfiguration getDonationConfiguration() {
|
||||
DonationConfiguration configuration = new DonationConfiguration();
|
||||
configuration.setApiKey("test-api-key");
|
||||
configuration.setDescription("some description");
|
||||
configuration.setUri("http://localhost:" + wireMockRule.port() + "/foo/bar");
|
||||
configuration.setUri("http://localhost:" + wm.getRuntimeInfo().getHttpPort() + "/foo/bar");
|
||||
configuration.setCircuitBreaker(new CircuitBreakerConfiguration());
|
||||
configuration.setRetry(new RetryConfiguration());
|
||||
configuration.setSupportedCurrencies(Set.of("usd", "gbp"));
|
||||
return configuration;
|
||||
}
|
||||
|
||||
static BadgesConfiguration getBadgesConfiguration() {
|
||||
return new BadgesConfiguration(
|
||||
List.of(
|
||||
new BadgeConfiguration("TEST", "other", "l", "m", "h", "x", "xx", "xxx", "s", "S"),
|
||||
new BadgeConfiguration("TEST1", "testing", "l", "m", "h", "x", "xx", "xxx", "s", "S"),
|
||||
new BadgeConfiguration("TEST2", "testing", "l", "m", "h", "x", "xx", "xxx", "s", "S"),
|
||||
new BadgeConfiguration("TEST3", "testing", "l", "m", "h", "x", "xx", "xxx", "s", "S")),
|
||||
List.of("TEST"),
|
||||
Map.of(1L, "TEST1", 2L, "TEST2", 3L, "TEST3"));
|
||||
}
|
||||
|
||||
Clock clock;
|
||||
ServerZkReceiptOperations zkReceiptOperations;
|
||||
RedeemedReceiptsManager redeemedReceiptsManager;
|
||||
AccountsManager accountsManager;
|
||||
byte[] receiptSerialBytes;
|
||||
ReceiptSerial receiptSerial;
|
||||
byte[] presentation;
|
||||
DonationController.ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory;
|
||||
ReceiptCredentialPresentation receiptCredentialPresentation;
|
||||
ResourceExtension resources;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws Throwable {
|
||||
clock = mock(Clock.class);
|
||||
zkReceiptOperations = mock(ServerZkReceiptOperations.class);
|
||||
redeemedReceiptsManager = mock(RedeemedReceiptsManager.class);
|
||||
accountsManager = mock(AccountsManager.class);
|
||||
AccountsHelper.setupMockUpdate(accountsManager);
|
||||
receiptSerialBytes = new byte[ReceiptSerial.SIZE];
|
||||
secureRandom.nextBytes(receiptSerialBytes);
|
||||
receiptSerial = new ReceiptSerial(receiptSerialBytes);
|
||||
presentation = new byte[ReceiptCredentialPresentation.SIZE];
|
||||
secureRandom.nextBytes(presentation);
|
||||
receiptCredentialPresentationFactory = mock(DonationController.ReceiptCredentialPresentationFactory.class);
|
||||
receiptCredentialPresentation = mock(ReceiptCredentialPresentation.class);
|
||||
|
||||
when(clock.millis()).thenReturn(nowEpochSeconds * 1000L);
|
||||
when(clock.instant()).thenReturn(Instant.ofEpochSecond(nowEpochSeconds));
|
||||
|
||||
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)))
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new DonationController(executor, configuration))
|
||||
.addResource(new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager,
|
||||
getBadgesConfiguration(), receiptCredentialPresentationFactory, httpClientExecutor,
|
||||
getDonationConfiguration()))
|
||||
.build();
|
||||
resources.before();
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() throws Throwable {
|
||||
@AfterEach
|
||||
void afterEach() throws Throwable {
|
||||
resources.after();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetApplePayAuthorizationReturns200() {
|
||||
wireMockRule.stubFor(post(urlEqualTo("/foo/bar"))
|
||||
void testGetApplePayAuthorizationReturns200() {
|
||||
wm.stubFor(post(urlEqualTo("/foo/bar"))
|
||||
.withBasicAuth("test-api-key", "")
|
||||
.willReturn(aResponse()
|
||||
.withHeader("Content-Type", MediaType.APPLICATION_JSON)
|
||||
@@ -96,7 +177,7 @@ public class DonationControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetApplePayAuthorizationWithoutAuthHeaderReturns401() {
|
||||
void testGetApplePayAuthorizationWithoutAuthHeaderReturns401() {
|
||||
ApplePayAuthorizationRequest request = new ApplePayAuthorizationRequest();
|
||||
request.setCurrency("usd");
|
||||
request.setAmount(1000);
|
||||
@@ -109,7 +190,7 @@ public class DonationControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetApplePayAuthorizationWithUnsupportedCurrencyReturns422() {
|
||||
void testGetApplePayAuthorizationWithUnsupportedCurrencyReturns422() {
|
||||
ApplePayAuthorizationRequest request = new ApplePayAuthorizationRequest();
|
||||
request.setCurrency("zzz");
|
||||
request.setAmount(1000);
|
||||
@@ -121,4 +202,48 @@ public class DonationControllerTest {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@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.get(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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.ws.rs.client.Entity;
|
||||
@@ -115,7 +116,7 @@ class ProfileControllerTest {
|
||||
new BadgeConfiguration("TEST1", "testing", "l", "m", "h", "x", "xx", "xxx", "s", "S"),
|
||||
new BadgeConfiguration("TEST2", "testing", "l", "m", "h", "x", "xx", "xxx", "s", "S"),
|
||||
new BadgeConfiguration("TEST3", "testing", "l", "m", "h", "x", "xx", "xxx", "s", "S")
|
||||
), List.of("TEST1")),
|
||||
), List.of("TEST1"), Map.of(1L, "TEST1", 2L, "TEST2", 3L, "TEST3")),
|
||||
s3client,
|
||||
postPolicyGenerator,
|
||||
policySigner,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.tests.storage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtension;
|
||||
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
|
||||
|
||||
class RedeemedReceiptsManagerTest {
|
||||
|
||||
private static final long NOW_EPOCH_SECONDS = 1_500_000_000L;
|
||||
private static final String REDEEMED_RECEIPTS_TABLE_NAME = "redeemed_receipts";
|
||||
private static final SecureRandom SECURE_RANDOM;
|
||||
static {
|
||||
try {
|
||||
SECURE_RANDOM = SecureRandom.getInstanceStrong();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterExtension
|
||||
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
|
||||
.tableName(REDEEMED_RECEIPTS_TABLE_NAME)
|
||||
.hashKey(RedeemedReceiptsManager.KEY_SERIAL)
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(RedeemedReceiptsManager.KEY_SERIAL)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Clock clock;
|
||||
ReceiptSerial receiptSerial;
|
||||
RedeemedReceiptsManager redeemedReceiptsManager;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws InvalidInputException {
|
||||
clock = mock(Clock.class);
|
||||
when(clock.millis()).thenReturn(NOW_EPOCH_SECONDS * 1000L);
|
||||
when(clock.instant()).thenReturn(Instant.ofEpochSecond(NOW_EPOCH_SECONDS));
|
||||
byte[] receiptSerialBytes = new byte[ReceiptSerial.SIZE];
|
||||
SECURE_RANDOM.nextBytes(receiptSerialBytes);
|
||||
receiptSerial = new ReceiptSerial(receiptSerialBytes);
|
||||
redeemedReceiptsManager = new RedeemedReceiptsManager(
|
||||
clock, REDEEMED_RECEIPTS_TABLE_NAME, dynamoDbExtension.getDynamoDbAsyncClient(), Duration.ofDays(90));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPut() throws ExecutionException, InterruptedException {
|
||||
final long receiptExpiration = 42;
|
||||
final long receiptLevel = 3;
|
||||
CompletableFuture<Boolean> put;
|
||||
|
||||
// initial insert should return true
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID);
|
||||
assertThat(put.get()).isTrue();
|
||||
|
||||
// subsequent attempted inserts with modified parameters should return false
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration + 1, receiptLevel, AuthHelper.VALID_UUID);
|
||||
assertThat(put.get()).isFalse();
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel + 1, AuthHelper.VALID_UUID);
|
||||
assertThat(put.get()).isFalse();
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID_TWO);
|
||||
assertThat(put.get()).isFalse();
|
||||
|
||||
// repeated insert attempt of the original parameters should return true
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID);
|
||||
assertThat(put.get()).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user