mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 00:48:38 +01:00
Add playbilling endpoint to /v1/subscriptions
This commit is contained in:
@@ -12,6 +12,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
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.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -93,7 +94,7 @@ import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionPaymentProcessor;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.CustomerAwareSubscriptionPaymentProcessor;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
@@ -124,7 +125,7 @@ class SubscriptionControllerTest {
|
||||
private static final BankMandateTranslator BANK_MANDATE_TRANSLATOR = mock(BankMandateTranslator.class);
|
||||
private final static SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(CLOCK, SUBSCRIPTION_CONFIG,
|
||||
ONETIME_CONFIG, new SubscriptionManager(SUBSCRIPTIONS, List.of(STRIPE_MANAGER, BRAINTREE_MANAGER, PLAY_MANAGER), ZK_OPS,
|
||||
ISSUED_RECEIPTS_MANAGER), STRIPE_MANAGER, BRAINTREE_MANAGER, BADGE_TRANSLATOR, LEVEL_TRANSLATOR,
|
||||
ISSUED_RECEIPTS_MANAGER), STRIPE_MANAGER, BRAINTREE_MANAGER, PLAY_MANAGER, BADGE_TRANSLATOR, LEVEL_TRANSLATOR,
|
||||
BANK_MANDATE_TRANSLATOR);
|
||||
private static final OneTimeDonationController ONE_TIME_CONTROLLER = new OneTimeDonationController(CLOCK,
|
||||
ONETIME_CONFIG, STRIPE_MANAGER, BRAINTREE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER, ONE_TIME_DONATIONS_MANAGER);
|
||||
@@ -403,7 +404,7 @@ class SubscriptionControllerTest {
|
||||
@Test
|
||||
void createSubscriptionSuccess() {
|
||||
when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
|
||||
.thenReturn(CompletableFuture.completedFuture(mock(SubscriptionPaymentProcessor.SubscriptionId.class)));
|
||||
.thenReturn(CompletableFuture.completedFuture(mock(CustomerAwareSubscriptionPaymentProcessor.SubscriptionId.class)));
|
||||
|
||||
final String level = String.valueOf(levelId);
|
||||
final String idempotencyKey = UUID.randomUUID().toString();
|
||||
@@ -715,7 +716,7 @@ class SubscriptionControllerTest {
|
||||
.thenReturn(CompletableFuture.completedFuture(Subscriptions.GetResult.found(record)));
|
||||
|
||||
when(BRAINTREE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new SubscriptionPaymentProcessor.SubscriptionId(
|
||||
.thenReturn(CompletableFuture.completedFuture(new CustomerAwareSubscriptionPaymentProcessor.SubscriptionId(
|
||||
"subscription")));
|
||||
when(SUBSCRIPTIONS.subscriptionCreated(any(), any(), any(), anyLong()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
@@ -769,12 +770,12 @@ class SubscriptionControllerTest {
|
||||
.thenReturn(CompletableFuture.completedFuture(subscriptionObj));
|
||||
when(BRAINTREE_MANAGER.getLevelAndCurrencyForSubscription(subscriptionObj))
|
||||
.thenReturn(CompletableFuture.completedFuture(
|
||||
new SubscriptionPaymentProcessor.LevelAndCurrency(existingLevel, existingCurrency)));
|
||||
new CustomerAwareSubscriptionPaymentProcessor.LevelAndCurrency(existingLevel, existingCurrency)));
|
||||
final String updatedSubscriptionId = "updatedSubscriptionId";
|
||||
|
||||
if (expectUpdate) {
|
||||
when(BRAINTREE_MANAGER.updateSubscription(any(), any(), anyLong(), anyString()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new SubscriptionPaymentProcessor.SubscriptionId(
|
||||
.thenReturn(CompletableFuture.completedFuture(new CustomerAwareSubscriptionPaymentProcessor.SubscriptionId(
|
||||
updatedSubscriptionId)));
|
||||
when(SUBSCRIPTIONS.subscriptionLevelChanged(any(), any(), anyLong(), anyString()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
@@ -844,7 +845,7 @@ class SubscriptionControllerTest {
|
||||
.thenReturn(CompletableFuture.completedFuture(subscriptionObj));
|
||||
when(BRAINTREE_MANAGER.getLevelAndCurrencyForSubscription(subscriptionObj))
|
||||
.thenReturn(CompletableFuture.completedFuture(
|
||||
new SubscriptionPaymentProcessor.LevelAndCurrency(201, "usd")));
|
||||
new CustomerAwareSubscriptionPaymentProcessor.LevelAndCurrency(201, "usd")));
|
||||
|
||||
// Try to change from a backup subscription (201) to a donation subscription (5)
|
||||
final Response response = RESOURCE_EXTENSION
|
||||
@@ -860,6 +861,50 @@ class SubscriptionControllerTest {
|
||||
.isEqualTo(SubscriptionController.SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void setPlayPurchaseToken() {
|
||||
final String purchaseToken = "aPurchaseToken";
|
||||
final byte[] subscriberUserAndKey = new byte[32];
|
||||
Arrays.fill(subscriberUserAndKey, (byte) 1);
|
||||
final byte[] user = Arrays.copyOfRange(subscriberUserAndKey, 0, 16);
|
||||
final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey);
|
||||
|
||||
final Instant now = Instant.now();
|
||||
when(CLOCK.instant()).thenReturn(now);
|
||||
|
||||
final Map<String, AttributeValue> dynamoItem = Map.of(Subscriptions.KEY_PASSWORD, b(new byte[16]),
|
||||
Subscriptions.KEY_CREATED_AT, n(Instant.now().getEpochSecond()),
|
||||
Subscriptions.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond())
|
||||
);
|
||||
final Subscriptions.Record record = Subscriptions.Record.from(user, dynamoItem);
|
||||
when(SUBSCRIPTIONS.get(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(Subscriptions.GetResult.found(record)));
|
||||
|
||||
final GooglePlayBillingManager.ValidatedToken validatedToken = mock(GooglePlayBillingManager.ValidatedToken.class);
|
||||
when(validatedToken.getLevel()).thenReturn(99L);
|
||||
when(validatedToken.acknowledgePurchase()).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(PLAY_MANAGER.validateToken(eq(purchaseToken))).thenReturn(CompletableFuture.completedFuture(validatedToken));
|
||||
|
||||
when(SUBSCRIPTIONS.setIapPurchase(any(), any(), anyString(), anyLong(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
final Response response = RESOURCE_EXTENSION
|
||||
.target(String.format("/v1/subscription/%s/playbilling/%s", subscriberId, purchaseToken))
|
||||
.request()
|
||||
.post(Entity.json(""));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelSuccessResponse.class).level())
|
||||
.isEqualTo(99L);
|
||||
|
||||
verify(SUBSCRIPTIONS, times(1)).setIapPurchase(
|
||||
any(),
|
||||
eq(new ProcessorCustomer(purchaseToken, PaymentProvider.GOOGLE_PLAY_BILLING)),
|
||||
eq(purchaseToken),
|
||||
eq(99L),
|
||||
eq(now));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5, P45D", "201, P13D"})
|
||||
public void createReceiptCredential(long level, Duration expectedExpirationWindow)
|
||||
@@ -887,7 +932,7 @@ class SubscriptionControllerTest {
|
||||
when(SUBSCRIPTIONS.get(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(Subscriptions.GetResult.found(record)));
|
||||
when(BRAINTREE_MANAGER.getReceiptItem(subscriptionId)).thenReturn(
|
||||
CompletableFuture.completedFuture(new SubscriptionPaymentProcessor.ReceiptItem(
|
||||
CompletableFuture.completedFuture(new CustomerAwareSubscriptionPaymentProcessor.ReceiptItem(
|
||||
"itemId",
|
||||
PaymentTime.periodStart(Instant.ofEpochSecond(10).plus(Duration.ofDays(1))),
|
||||
level
|
||||
|
||||
@@ -16,9 +16,15 @@ 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.BasePlan;
|
||||
import com.google.api.services.androidpublisher.model.Money;
|
||||
import com.google.api.services.androidpublisher.model.OfferDetails;
|
||||
import com.google.api.services.androidpublisher.model.RegionalBasePlanConfig;
|
||||
import com.google.api.services.androidpublisher.model.Subscription;
|
||||
import com.google.api.services.androidpublisher.model.SubscriptionPurchaseLineItem;
|
||||
import com.google.api.services.androidpublisher.model.SubscriptionPurchaseV2;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
@@ -32,7 +38,6 @@ 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;
|
||||
@@ -56,6 +61,10 @@ class GooglePlayBillingManagerTest {
|
||||
private final AndroidPublisher.Purchases.Subscriptions.Cancel cancel =
|
||||
mock(AndroidPublisher.Purchases.Subscriptions.Cancel.class);
|
||||
|
||||
// Returned in response to a monetization.subscriptions.get
|
||||
private final AndroidPublisher.Monetization.Subscriptions.Get subscriptionConfig =
|
||||
mock(AndroidPublisher.Monetization.Subscriptions.Get.class);
|
||||
|
||||
private final MutableClock clock = MockUtils.mutableClock(0L);
|
||||
|
||||
private ExecutorService executor;
|
||||
@@ -68,9 +77,12 @@ class GooglePlayBillingManagerTest {
|
||||
|
||||
AndroidPublisher androidPublisher = mock(AndroidPublisher.class);
|
||||
AndroidPublisher.Purchases purchases = mock(AndroidPublisher.Purchases.class);
|
||||
AndroidPublisher.Monetization monetization = mock(AndroidPublisher.Monetization.class);
|
||||
|
||||
when(androidPublisher.purchases()).thenReturn(purchases);
|
||||
when(androidPublisher.monetization()).thenReturn(monetization);
|
||||
|
||||
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);
|
||||
|
||||
@@ -81,6 +93,11 @@ class GooglePlayBillingManagerTest {
|
||||
when(subscriptions.cancel(PACKAGE_NAME, PRODUCT_ID, PURCHASE_TOKEN))
|
||||
.thenReturn(cancel);
|
||||
|
||||
AndroidPublisher.Monetization.Subscriptions msubscriptions = mock(
|
||||
AndroidPublisher.Monetization.Subscriptions.class);
|
||||
when(monetization.subscriptions()).thenReturn(msubscriptions);
|
||||
when(msubscriptions.get(PACKAGE_NAME, PRODUCT_ID)).thenReturn(subscriptionConfig);
|
||||
|
||||
executor = Executors.newSingleThreadExecutor();
|
||||
googlePlayBillingManager = new GooglePlayBillingManager(
|
||||
androidPublisher, clock, PACKAGE_NAME, Map.of(PRODUCT_ID, 201L), executor);
|
||||
@@ -186,7 +203,7 @@ class GooglePlayBillingManagerTest {
|
||||
.setProductId(PRODUCT_ID))));
|
||||
|
||||
clock.setTimeInstant(day9);
|
||||
SubscriptionManager.Processor.ReceiptItem item = googlePlayBillingManager.getReceiptItem(PURCHASE_TOKEN).join();
|
||||
SubscriptionPaymentProcessor.ReceiptItem item = googlePlayBillingManager.getReceiptItem(PURCHASE_TOKEN).join();
|
||||
assertThat(item.itemId()).isEqualTo(ORDER_ID);
|
||||
assertThat(item.level()).isEqualTo(201L);
|
||||
|
||||
@@ -207,4 +224,33 @@ class GooglePlayBillingManagerTest {
|
||||
googlePlayBillingManager.getReceiptItem(PURCHASE_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSubscriptionInfo() throws IOException {
|
||||
final String basePlanId = "basePlanId";
|
||||
when(subscriptionsv2Get.execute()).thenReturn(new SubscriptionPurchaseV2()
|
||||
.setAcknowledgementState(GooglePlayBillingManager.AcknowledgementState.ACKNOWLEDGED.apiString())
|
||||
.setSubscriptionState(GooglePlayBillingManager.SubscriptionState.ACTIVE.apiString())
|
||||
.setLatestOrderId(ORDER_ID)
|
||||
.setRegionCode("US")
|
||||
.setLineItems(List.of(new SubscriptionPurchaseLineItem()
|
||||
.setExpiryTime(Instant.now().plus(Duration.ofDays(1)).toString())
|
||||
.setProductId(PRODUCT_ID)
|
||||
.setOfferDetails(new OfferDetails().setBasePlanId(basePlanId)))));
|
||||
|
||||
final BasePlan basePlan = new BasePlan()
|
||||
.setBasePlanId(basePlanId)
|
||||
.setRegionalConfigs(List.of(
|
||||
new RegionalBasePlanConfig()
|
||||
.setRegionCode("US")
|
||||
.setPrice(new Money().setCurrencyCode("USD").setUnits(1L).setNanos(750_000_000))));
|
||||
when(subscriptionConfig.execute()).thenReturn(new Subscription().setBasePlans(List.of(basePlan)));
|
||||
|
||||
final SubscriptionInformation info = googlePlayBillingManager.getSubscriptionInformation(PURCHASE_TOKEN).join();
|
||||
assertThat(info.active()).isTrue();
|
||||
assertThat(info.paymentProcessing()).isFalse();
|
||||
assertThat(info.price().currency()).isEqualTo("USD");
|
||||
assertThat(info.price().amount().compareTo(new BigDecimal("175"))).isEqualTo(0); // 175 cents
|
||||
assertThat(info.level()).isEqualTo(201L);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user