diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java index 5b44976ad..5649e6255 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java @@ -142,7 +142,7 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor { // We've already acknowledged this purchase on a previous attempt, nothing to do return CompletableFuture.completedFuture(null); } - return executeAsync(pub -> pub.purchases().subscriptions() + return executeTokenOperation(pub -> pub.purchases().subscriptions() .acknowledge(packageName, productId, purchaseToken, new SubscriptionPurchasesAcknowledgeRequest())); } @@ -217,7 +217,7 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor { } final SubscriptionPurchaseLineItem purchase = getLineItem(subscription); - return executeAsync(pub -> + return executeTokenOperation(pub -> pub.purchases().subscriptions().cancel(packageName, purchase.getProductId(), purchaseToken)); }); } @@ -339,26 +339,37 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor { return CompletableFuture.supplyAsync(() -> { try { return apiCall.req(androidPublisher).execute(); - } catch (GoogleJsonResponseException e) { - if (e.getStatusCode() == Response.Status.NOT_FOUND.getStatusCode()) { - throw ExceptionUtils.wrap(new SubscriptionException.NotFound()); - } - logger.warn("Unexpected HTTP status code {} from androidpublisher: {}", e.getStatusCode(), e.getDetails(), e); - throw ExceptionUtils.wrap(e); - } catch (HttpResponseException e) { - if (e.getStatusCode() == Response.Status.NOT_FOUND.getStatusCode()) { - throw ExceptionUtils.wrap(new SubscriptionException.NotFound()); - } - logger.warn("Unexpected HTTP status code {} from androidpublisher", e.getStatusCode(), e); - throw ExceptionUtils.wrap(e); } catch (IOException e) { throw ExceptionUtils.wrap(e); } }, executor); } + /** + * Asynchronously execute a synchronous API call on a purchaseToken, mapping expected errors to the appropriate + * {@link SubscriptionException} + * + * @param apiCall An API call that operates on a purchaseToken + * @param The result of the API call + * @return A stage that completes with the result of the API call + */ + private CompletableFuture executeTokenOperation(final ApiCall apiCall) { + return executeAsync(apiCall) + .exceptionally(ExceptionUtils.exceptionallyHandler(HttpResponseException.class, e -> { + if (e.getStatusCode() == Response.Status.NOT_FOUND.getStatusCode() + || e.getStatusCode() == Response.Status.GONE.getStatusCode()) { + throw ExceptionUtils.wrap(new SubscriptionException.NotFound()); + } + final String details = e instanceof GoogleJsonResponseException + ? ((GoogleJsonResponseException) e).getDetails().toString() + : ""; + logger.warn("Unexpected HTTP status code {} from androidpublisher: {}", e.getStatusCode(), details, e); + throw ExceptionUtils.wrap(e); + })); + } + private CompletableFuture lookupSubscription(final String purchaseToken) { - return executeAsync(publisher -> publisher.purchases().subscriptionsv2().get(packageName, purchaseToken)); + return executeTokenOperation(publisher -> publisher.purchases().subscriptionsv2().get(packageName, purchaseToken)); } private long productIdToLevel(final String productId) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManagerTest.java index 767e4fd71..c47b4ea67 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManagerTest.java @@ -15,6 +15,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import com.google.api.client.http.HttpResponseException; import com.google.api.services.androidpublisher.AndroidPublisher; import com.google.api.services.androidpublisher.model.AutoRenewingPlan; import com.google.api.services.androidpublisher.model.BasePlan; @@ -33,11 +34,14 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.whispersystems.textsecuregcm.storage.SubscriptionException; import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil; import org.whispersystems.textsecuregcm.util.MockUtils; @@ -257,4 +261,22 @@ class GooglePlayBillingManagerTest { } + public static Stream tokenErrors() { + return Stream.of( + Arguments.of(404, SubscriptionException.NotFound.class), + Arguments.of(410, SubscriptionException.NotFound.class), + Arguments.of(400, HttpResponseException.class) + ); + } + @ParameterizedTest + @MethodSource + public void tokenErrors(final int httpStatus, Class expected) throws IOException { + final HttpResponseException mockException = mock(HttpResponseException.class); + when(mockException.getStatusCode()).thenReturn(httpStatus); + when(subscriptionsv2Get.execute()).thenThrow(mockException); + + CompletableFutureTestUtil.assertFailsWithCause(expected, + googlePlayBillingManager.getSubscriptionInformation(PURCHASE_TOKEN)); + } + }