Add GooglePlayBillingManager

This commit is contained in:
ravi-signal
2024-08-28 14:22:37 -05:00
committed by GitHub
parent 9249cf240e
commit 176a15dace
24 changed files with 999 additions and 39 deletions

View File

@@ -0,0 +1,210 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.subscriptions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.model.SubscriptionPurchaseLineItem;
import com.google.api.services.androidpublisher.model.SubscriptionPurchaseV2;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
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.EnumSource;
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
import org.whispersystems.textsecuregcm.util.MockUtils;
import org.whispersystems.textsecuregcm.util.MutableClock;
class GooglePlayBillingManagerTest {
private static final String PRODUCT_ID = "productId";
private static final String PACKAGE_NAME = "package.name";
private static final String PURCHASE_TOKEN = "purchaseToken";
private static final String ORDER_ID = "orderId";
// Returned in response to a purchases.subscriptionsv2.get
private final AndroidPublisher.Purchases.Subscriptionsv2.Get subscriptionsv2Get =
mock(AndroidPublisher.Purchases.Subscriptionsv2.Get.class);
// Returned in response to a purchases.subscriptions.acknowledge
private final AndroidPublisher.Purchases.Subscriptions.Acknowledge acknowledge =
mock(AndroidPublisher.Purchases.Subscriptions.Acknowledge.class);
// Returned in response to a purchases.subscriptionscancel.
private final AndroidPublisher.Purchases.Subscriptions.Cancel cancel =
mock(AndroidPublisher.Purchases.Subscriptions.Cancel.class);
private final MutableClock clock = MockUtils.mutableClock(0L);
private ExecutorService executor;
private GooglePlayBillingManager googlePlayBillingManager;
@BeforeEach
public void setup() throws IOException {
reset(subscriptionsv2Get);
clock.setTimeMillis(0L);
AndroidPublisher androidPublisher = mock(AndroidPublisher.class);
AndroidPublisher.Purchases purchases = mock(AndroidPublisher.Purchases.class);
AndroidPublisher.Purchases.Subscriptionsv2 subscriptionsv2 = mock(AndroidPublisher.Purchases.Subscriptionsv2.class);
when(androidPublisher.purchases()).thenReturn(purchases);
when(purchases.subscriptionsv2()).thenReturn(subscriptionsv2);
when(subscriptionsv2.get(PACKAGE_NAME, PURCHASE_TOKEN)).thenReturn(subscriptionsv2Get);
AndroidPublisher.Purchases.Subscriptions subscriptions = mock(AndroidPublisher.Purchases.Subscriptions.class);
when(purchases.subscriptions()).thenReturn(subscriptions);
when(subscriptions.acknowledge(eq(PACKAGE_NAME), eq(PRODUCT_ID), eq(PURCHASE_TOKEN), any()))
.thenReturn(acknowledge);
when(subscriptions.cancel(PACKAGE_NAME, PRODUCT_ID, PURCHASE_TOKEN))
.thenReturn(cancel);
executor = Executors.newSingleThreadExecutor();
googlePlayBillingManager = new GooglePlayBillingManager(
androidPublisher, clock, PACKAGE_NAME, Map.of(PRODUCT_ID, 201L), executor);
}
@AfterEach
public void teardown() throws InterruptedException {
executor.shutdownNow();
executor.awaitTermination(1, TimeUnit.SECONDS);
}
@Test
public void validatePurchase() throws IOException {
when(subscriptionsv2Get.execute()).thenReturn(new SubscriptionPurchaseV2()
.setAcknowledgementState(GooglePlayBillingManager.AcknowledgementState.PENDING.apiString())
.setSubscriptionState(GooglePlayBillingManager.SubscriptionState.ACTIVE.apiString())
.setLineItems(List.of(new SubscriptionPurchaseLineItem()
.setExpiryTime(Instant.now().plus(Duration.ofDays(1)).toString())
.setProductId(PRODUCT_ID))));
final GooglePlayBillingManager.ValidatedToken result = googlePlayBillingManager
.validateToken(PURCHASE_TOKEN).join();
assertThat(result.getLevel()).isEqualTo(201);
assertThatNoException().isThrownBy(() -> result.acknowledgePurchase().join());
verify(acknowledge, times(1)).execute();
}
@ParameterizedTest
@EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"ACTIVE"})
public void rejectInactivePurchase(GooglePlayBillingManager.SubscriptionState subscriptionState) throws IOException {
when(subscriptionsv2Get.execute()).thenReturn(new SubscriptionPurchaseV2()
.setAcknowledgementState(GooglePlayBillingManager.AcknowledgementState.PENDING.apiString())
.setSubscriptionState(subscriptionState.apiString())
.setLineItems(List.of(new SubscriptionPurchaseLineItem()
.setExpiryTime(Instant.now().plus(Duration.ofDays(1)).toString())
.setProductId(PRODUCT_ID))));
CompletableFutureTestUtil.assertFailsWithCause(
SubscriptionException.PaymentRequired.class,
googlePlayBillingManager.validateToken(PURCHASE_TOKEN));
}
@Test
public void avoidDoubleAcknowledge() throws IOException {
when(subscriptionsv2Get.execute()).thenReturn(new SubscriptionPurchaseV2()
.setAcknowledgementState(GooglePlayBillingManager.AcknowledgementState.ACKNOWLEDGED.apiString())
.setSubscriptionState(GooglePlayBillingManager.SubscriptionState.ACTIVE.apiString())
.setLineItems(List.of(new SubscriptionPurchaseLineItem()
.setExpiryTime(Instant.now().plus(Duration.ofDays(1)).toString())
.setProductId(PRODUCT_ID))));
final GooglePlayBillingManager.ValidatedToken result = googlePlayBillingManager
.validateToken(PURCHASE_TOKEN).join();
assertThat(result.getLevel()).isEqualTo(201);
assertThatNoException().isThrownBy(() -> result.acknowledgePurchase().join());
verifyNoInteractions(acknowledge);
}
@ParameterizedTest
@EnumSource
public void cancel(GooglePlayBillingManager.SubscriptionState subscriptionState) throws IOException {
when(subscriptionsv2Get.execute()).thenReturn(new SubscriptionPurchaseV2()
.setAcknowledgementState(GooglePlayBillingManager.AcknowledgementState.ACKNOWLEDGED.apiString())
.setSubscriptionState(subscriptionState.apiString())
.setLineItems(List.of(new SubscriptionPurchaseLineItem()
.setExpiryTime(Instant.now().plus(Duration.ofDays(1)).toString())
.setProductId(PRODUCT_ID))));
assertThatNoException().isThrownBy(() ->
googlePlayBillingManager.cancelAllActiveSubscriptions(PURCHASE_TOKEN).join());
final int wanted = switch (subscriptionState) {
case CANCELED, EXPIRED -> 0;
default -> 1;
};
verify(cancel, times(wanted)).execute();
}
@Test
public void getReceiptUnacknowledged() throws IOException {
when(subscriptionsv2Get.execute()).thenReturn(new SubscriptionPurchaseV2()
.setAcknowledgementState(GooglePlayBillingManager.AcknowledgementState.PENDING.apiString())
.setSubscriptionState(GooglePlayBillingManager.SubscriptionState.ACTIVE.apiString())
.setLineItems(List.of(new SubscriptionPurchaseLineItem()
.setExpiryTime(Instant.now().plus(Duration.ofDays(1)).toString())
.setProductId(PRODUCT_ID))));
CompletableFutureTestUtil.assertFailsWithCause(
IllegalStateException.class,
googlePlayBillingManager.getReceiptItem(PURCHASE_TOKEN));
}
@Test
public void getReceiptExpiring() throws IOException {
final Instant day9 = Instant.EPOCH.plus(Duration.ofDays(9));
final Instant day10 = Instant.EPOCH.plus(Duration.ofDays(10));
when(subscriptionsv2Get.execute()).thenReturn(new SubscriptionPurchaseV2()
.setAcknowledgementState(GooglePlayBillingManager.AcknowledgementState.ACKNOWLEDGED.apiString())
.setSubscriptionState(GooglePlayBillingManager.SubscriptionState.CANCELED.apiString())
.setLatestOrderId(ORDER_ID)
.setLineItems(List.of(new SubscriptionPurchaseLineItem()
.setExpiryTime(day10.toString().toString())
.setProductId(PRODUCT_ID))));
clock.setTimeInstant(day9);
SubscriptionManager.Processor.ReceiptItem item = googlePlayBillingManager.getReceiptItem(PURCHASE_TOKEN).join();
assertThat(item.itemId()).isEqualTo(ORDER_ID);
assertThat(item.level()).isEqualTo(201L);
// receipt expirations rounded to nearest next day
assertThat(item.paymentTime().receiptExpiration(Duration.ofDays(1), Duration.ZERO))
.isEqualTo(day10.plus(Duration.ofDays(1)));
// should still be able to get a receipt the next day
clock.setTimeInstant(day10);
item = googlePlayBillingManager.getReceiptItem(PURCHASE_TOKEN).join();
assertThat(item.itemId()).isEqualTo(ORDER_ID);
// next second should be expired
clock.setTimeInstant(day10.plus(Duration.ofSeconds(1)));
CompletableFutureTestUtil.assertFailsWithCause(
SubscriptionException.PaymentRequired.class,
googlePlayBillingManager.getReceiptItem(PURCHASE_TOKEN));
}
}