mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-22 02:58:02 +01:00
Add support for one-time PayPal donations
This commit is contained in:
@@ -19,8 +19,8 @@ import static org.whispersystems.textsecuregcm.util.AttributeValues.n;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.stripe.exception.ApiException;
|
||||
import com.stripe.model.Subscription;
|
||||
import com.stripe.model.PaymentIntent;
|
||||
import com.stripe.model.Subscription;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
@@ -61,7 +61,7 @@ import org.whispersystems.textsecuregcm.entities.BadgeSvg;
|
||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
|
||||
@@ -87,18 +87,21 @@ class SubscriptionControllerTest {
|
||||
private static final PaymentIntent PAYMENT_INTENT = mock(PaymentIntent.class);
|
||||
|
||||
static {
|
||||
// this behavior is required by the SubscriptionController constructor
|
||||
when(STRIPE_MANAGER.getSupportedCurrencies())
|
||||
.thenCallRealMethod();
|
||||
when(STRIPE_MANAGER.supportsPaymentMethod(PaymentMethod.CARD))
|
||||
.thenReturn(Set.of("usd", "jpy", "bif"));
|
||||
when(STRIPE_MANAGER.supportsPaymentMethod(any()))
|
||||
.thenCallRealMethod();
|
||||
}
|
||||
|
||||
private static final BraintreeManager BRAINTREE_MANAGER = mock(BraintreeManager.class);
|
||||
|
||||
private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class);
|
||||
private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class);
|
||||
private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class);
|
||||
private static final LevelTranslator LEVEL_TRANSLATOR = mock(LevelTranslator.class);
|
||||
private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
|
||||
CLOCK, SUBSCRIPTION_CONFIG, ONETIME_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS,
|
||||
CLOCK, SUBSCRIPTION_CONFIG, ONETIME_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, BRAINTREE_MANAGER, ZK_OPS,
|
||||
ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR, LEVEL_TRANSLATOR);
|
||||
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
|
||||
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
||||
@@ -157,8 +160,9 @@ class SubscriptionControllerTest {
|
||||
@Test
|
||||
void testCreateBoostPaymentIntent() {
|
||||
when(STRIPE_MANAGER.convertConfiguredAmountToStripeAmount(any(), any())).thenReturn(new BigDecimal(300));
|
||||
when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong()))
|
||||
when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong()))
|
||||
.thenReturn(CompletableFuture.completedFuture(PAYMENT_INTENT));
|
||||
when(STRIPE_MANAGER.supportsCurrency("usd")).thenReturn(true);
|
||||
|
||||
String clientSecret = "some_client_secret";
|
||||
when(PAYMENT_INTENT.getClientSecret()).thenReturn(clientSecret);
|
||||
@@ -168,7 +172,7 @@ class SubscriptionControllerTest {
|
||||
.post(Entity.json("{\"currency\": \"USD\", \"amount\": 300, \"level\": null}"));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void createBoostReceiptInvalid() {
|
||||
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/receipt_credentials")
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
|
||||
|
||||
@@ -31,9 +32,9 @@ class IssuedReceiptsManagerTest {
|
||||
@RegisterExtension
|
||||
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
|
||||
.tableName(ISSUED_RECEIPTS_TABLE_NAME)
|
||||
.hashKey(IssuedReceiptsManager.KEY_STRIPE_ID)
|
||||
.hashKey(IssuedReceiptsManager.KEY_PROCESSOR_ITEM_ID)
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(IssuedReceiptsManager.KEY_STRIPE_ID)
|
||||
.attributeName(IssuedReceiptsManager.KEY_PROCESSOR_ITEM_ID)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.build())
|
||||
.build();
|
||||
@@ -59,18 +60,21 @@ class IssuedReceiptsManagerTest {
|
||||
byte[] request1 = new byte[20];
|
||||
SECURE_RANDOM.nextBytes(request1);
|
||||
when(receiptCredentialRequest.serialize()).thenReturn(request1);
|
||||
CompletableFuture<Void> future = issuedReceiptsManager.recordIssuance("item-1", receiptCredentialRequest, now);
|
||||
CompletableFuture<Void> future = issuedReceiptsManager.recordIssuance("item-1", SubscriptionProcessor.STRIPE,
|
||||
receiptCredentialRequest, now);
|
||||
assertThat(future).succeedsWithin(Duration.ofSeconds(3));
|
||||
|
||||
// same request should succeed
|
||||
future = issuedReceiptsManager.recordIssuance("item-1", receiptCredentialRequest, now);
|
||||
future = issuedReceiptsManager.recordIssuance("item-1", SubscriptionProcessor.STRIPE, receiptCredentialRequest,
|
||||
now);
|
||||
assertThat(future).succeedsWithin(Duration.ofSeconds(3));
|
||||
|
||||
// same item with new request should fail
|
||||
byte[] request2 = new byte[20];
|
||||
SECURE_RANDOM.nextBytes(request2);
|
||||
when(receiptCredentialRequest.serialize()).thenReturn(request2);
|
||||
future = issuedReceiptsManager.recordIssuance("item-1", receiptCredentialRequest, now);
|
||||
future = issuedReceiptsManager.recordIssuance("item-1", SubscriptionProcessor.STRIPE, receiptCredentialRequest,
|
||||
now);
|
||||
assertThat(future).failsWithin(Duration.ofSeconds(3)).
|
||||
withThrowableOfType(Throwable.class).
|
||||
havingCause().
|
||||
@@ -80,7 +84,8 @@ class IssuedReceiptsManagerTest {
|
||||
"status 409"));
|
||||
|
||||
// different item with new request should be okay though
|
||||
future = issuedReceiptsManager.recordIssuance("item-2", receiptCredentialRequest, now);
|
||||
future = issuedReceiptsManager.recordIssuance("item-2", SubscriptionProcessor.STRIPE, receiptCredentialRequest,
|
||||
now);
|
||||
assertThat(future).succeedsWithin(Duration.ofSeconds(3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.braintree.graphql.clientoperation.CreatePayPalOneTimePaymentMutation;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import javax.ws.rs.ServiceUnavailableException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
|
||||
class BraintreeGraphqlClientTest {
|
||||
|
||||
private static final String CURRENCY = "xts";
|
||||
private static final String RETURN_URL = "https://example.com/return";
|
||||
private static final String CANCEL_URL = "https://example.com/cancel";
|
||||
private static final String LOCALE = "xx";
|
||||
|
||||
private FaultTolerantHttpClient httpClient;
|
||||
private BraintreeGraphqlClient braintreeGraphqlClient;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
httpClient = mock(FaultTolerantHttpClient.class);
|
||||
|
||||
braintreeGraphqlClient = new BraintreeGraphqlClient(httpClient, "https://example.com", "public", "super-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPayPalOneTimePayment() {
|
||||
|
||||
final HttpResponse<Object> response = mock(HttpResponse.class);
|
||||
when(httpClient.sendAsync(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(response));
|
||||
|
||||
final String paymentId = "PAYID-AAA1AAAA1A11111AA111111A";
|
||||
when(response.body())
|
||||
.thenReturn(createPayPalOneTimePaymentResponse(paymentId));
|
||||
when(response.statusCode())
|
||||
.thenReturn(200);
|
||||
|
||||
final CompletableFuture<CreatePayPalOneTimePaymentMutation.CreatePayPalOneTimePayment> future = braintreeGraphqlClient.createPayPalOneTimePayment(
|
||||
BigDecimal.ONE, CURRENCY,
|
||||
RETURN_URL, CANCEL_URL, LOCALE);
|
||||
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(3), () -> {
|
||||
final CreatePayPalOneTimePaymentMutation.CreatePayPalOneTimePayment result = future.get();
|
||||
|
||||
assertEquals(paymentId, result.paymentId);
|
||||
assertNotNull(result.approvalUrl);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPayPalOneTimePaymentHttpError() {
|
||||
|
||||
final HttpResponse<Object> response = mock(HttpResponse.class);
|
||||
when(httpClient.sendAsync(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(response));
|
||||
|
||||
when(response.statusCode())
|
||||
.thenReturn(500);
|
||||
final HttpHeaders httpheaders = mock(HttpHeaders.class);
|
||||
when(httpheaders.firstValue(any())).thenReturn(Optional.empty());
|
||||
when(response.headers())
|
||||
.thenReturn(httpheaders);
|
||||
|
||||
final CompletableFuture<CreatePayPalOneTimePaymentMutation.CreatePayPalOneTimePayment> future = braintreeGraphqlClient.createPayPalOneTimePayment(
|
||||
BigDecimal.ONE, CURRENCY,
|
||||
RETURN_URL, CANCEL_URL, LOCALE);
|
||||
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(3), () -> {
|
||||
|
||||
final ExecutionException e = assertThrows(ExecutionException.class, future::get);
|
||||
|
||||
assertTrue(e.getCause() instanceof ServiceUnavailableException);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPayPalOneTimePaymentGraphQlError() {
|
||||
|
||||
final HttpResponse<Object> response = mock(HttpResponse.class);
|
||||
when(httpClient.sendAsync(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(response));
|
||||
|
||||
when(response.body())
|
||||
.thenReturn(createErrorResponse("createPayPalOneTimePayment", "12345"));
|
||||
when(response.statusCode())
|
||||
.thenReturn(200);
|
||||
|
||||
final CompletableFuture<CreatePayPalOneTimePaymentMutation.CreatePayPalOneTimePayment> future = braintreeGraphqlClient.createPayPalOneTimePayment(
|
||||
BigDecimal.ONE, CURRENCY,
|
||||
RETURN_URL, CANCEL_URL, LOCALE);
|
||||
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(3), () -> {
|
||||
|
||||
final ExecutionException e = assertThrows(ExecutionException.class, future::get);
|
||||
assertTrue(e.getCause() instanceof ServiceUnavailableException);
|
||||
});
|
||||
}
|
||||
|
||||
private String createPayPalOneTimePaymentResponse(final String paymentId) {
|
||||
final String cannedToken = "EC-1AA11111AA111111A";
|
||||
return String.format("""
|
||||
{
|
||||
"data": {
|
||||
"createPayPalOneTimePayment": {
|
||||
"approvalUrl": "https://www.sandbox.paypal.com/checkoutnow?nolegacy=1&token=%2$s",
|
||||
"paymentId": "%1$s"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"requestId": "%3$s"
|
||||
}
|
||||
}
|
||||
""", paymentId, cannedToken, UUID.randomUUID());
|
||||
}
|
||||
|
||||
private String createErrorResponse(final String operationName, final String legacyCode) {
|
||||
return String.format("""
|
||||
{
|
||||
"data": {
|
||||
"%1$s": null
|
||||
},
|
||||
"errors": [ {
|
||||
"message": "This is a test error message.",
|
||||
"locations": [ {
|
||||
"line": 2,
|
||||
"column": 7
|
||||
} ],
|
||||
"path": [ "%1$s" ],
|
||||
"extensions": {
|
||||
"errorType": "user_error",
|
||||
"errorClass": "VALIDATION",
|
||||
"legacyCode": "%2$s",
|
||||
"inputPath": [ "input", "testField" ]
|
||||
}
|
||||
}],
|
||||
"extensions": {
|
||||
"requestId": "%3$s"
|
||||
}
|
||||
}
|
||||
""", operationName, legacyCode, UUID.randomUUID());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tokenizePayPalOneTimePayment() {
|
||||
}
|
||||
|
||||
@Test
|
||||
void chargeOneTimePayment() {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user