From 487b5edc757452e4b53461cc66393540483bb96a Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Tue, 25 Apr 2023 17:12:16 -0500 Subject: [PATCH] Handle potentially null payment method when canceling subscription --- .../subscriptions/BraintreeManager.java | 36 ++++++++----- .../subscriptions/BraintreeManagerTest.java | 52 +++++++++++++++++++ 2 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManagerTest.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java index e57b01a49..5be93ac18 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java @@ -21,6 +21,7 @@ import com.braintreegateway.exceptions.NotFoundException; import com.fasterxml.jackson.core.JsonProcessingException; import java.math.BigDecimal; import java.time.Instant; +import java.util.Collections; import java.util.Comparator; import java.util.HexFormat; import java.util.List; @@ -35,6 +36,7 @@ import javax.annotation.Nullable; import javax.ws.rs.ClientErrorException; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; @@ -61,18 +63,26 @@ public class BraintreeManager implements SubscriptionProcessorManager { final CircuitBreakerConfiguration circuitBreakerConfiguration, final Executor executor) { - this.braintreeGateway = new BraintreeGateway(braintreeEnvironment, braintreeMerchantId, braintreePublicKey, - braintreePrivateKey); + this(new BraintreeGateway(braintreeEnvironment, braintreeMerchantId, braintreePublicKey, + braintreePrivateKey), + supportedCurrencies, + currenciesToMerchantAccounts, + new BraintreeGraphqlClient(FaultTolerantHttpClient.newBuilder() + .withName("braintree-graphql") + .withCircuitBreaker(circuitBreakerConfiguration) + .withExecutor(executor) + .build(), graphqlUri, braintreePublicKey, braintreePrivateKey), + executor); + } + + @VisibleForTesting + BraintreeManager(final BraintreeGateway braintreeGateway, final Set supportedCurrencies, + final Map currenciesToMerchantAccounts, final BraintreeGraphqlClient braintreeGraphqlClient, + final Executor executor) { + this.braintreeGateway = braintreeGateway; this.supportedCurrencies = supportedCurrencies; this.currenciesToMerchantAccounts = currenciesToMerchantAccounts; - - final FaultTolerantHttpClient httpClient = FaultTolerantHttpClient.newBuilder() - .withName("braintree-graphql") - .withCircuitBreaker(circuitBreakerConfiguration) - .withExecutor(executor) - .build(); - this.braintreeGraphqlClient = new BraintreeGraphqlClient(httpClient, graphqlUri, braintreePublicKey, - braintreePrivateKey); + this.braintreeGraphqlClient = braintreeGraphqlClient; this.executor = executor; } @@ -96,7 +106,6 @@ public class BraintreeManager implements SubscriptionProcessorManager { return supportedCurrencies.contains(currency.toLowerCase(Locale.ROOT)); } - @Override public CompletableFuture getPaymentDetails(final String paymentId) { return CompletableFuture.supplyAsync(() -> { @@ -446,7 +455,10 @@ public class BraintreeManager implements SubscriptionProcessorManager { return CompletableFuture.supplyAsync(() -> braintreeGateway.customer().find(customerId), executor).thenCompose(customer -> { - final List> subscriptionCancelFutures = customer.getDefaultPaymentMethod().getSubscriptions().stream() + final List> subscriptionCancelFutures = Optional.ofNullable(customer.getDefaultPaymentMethod()) + .map(com.braintreegateway.PaymentMethod::getSubscriptions) + .orElse(Collections.emptyList()) + .stream() .map(this::cancelSubscriptionAtEndOfCurrentPeriod) .toList(); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManagerTest.java new file mode 100644 index 000000000..924dcee83 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManagerTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.braintreegateway.BraintreeGateway; +import com.braintreegateway.Customer; +import com.braintreegateway.CustomerGateway; +import java.time.Duration; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BraintreeManagerTest { + + private BraintreeGateway braintreeGateway; + private BraintreeManager braintreeManager; + + @BeforeEach + void setup() { + braintreeGateway = mock(BraintreeGateway.class); + braintreeManager = new BraintreeManager(braintreeGateway, + Set.of("usd"), + Map.of("usd", "usdMerchant"), + mock(BraintreeGraphqlClient.class), + Executors.newSingleThreadExecutor()); + } + + @Test + void cancelAllActiveSubscriptions_nullDefaultPaymentMethod() { + + final Customer customer = mock(Customer.class); + when(customer.getDefaultPaymentMethod()).thenReturn(null); + + final CustomerGateway customerGateway = mock(CustomerGateway.class); + when(customerGateway.find(anyString())).thenReturn(customer); + + when(braintreeGateway.customer()).thenReturn(customerGateway); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> + braintreeManager.cancelAllActiveSubscriptions("customerId")).join(); + } +}