Treat a 410 for an IAP token as not found

This commit is contained in:
ravi-signal 2025-03-07 15:24:33 -06:00 committed by GitHub
parent d1c9dff2c5
commit 469955aec9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 48 additions and 15 deletions

View File

@ -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 <R> The result of the API call
* @return A stage that completes with the result of the API call
*/
private <R> CompletableFuture<R> executeTokenOperation(final ApiCall<R> 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<SubscriptionPurchaseV2> 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) {

View File

@ -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<Arguments> 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<? extends Exception> 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));
}
}