From e8978ef91c04279662ed23482ea6d3ce5ecfce05 Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Wed, 18 Jan 2023 12:53:29 -0600 Subject: [PATCH] Add tests for `SubscriptionController#setSubscriptionLevel` --- .../SubscriptionControllerTest.java | 187 ++++++++++++++++-- 1 file changed, 174 insertions(+), 13 deletions(-) diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java index c8db739f0..b14405b90 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java @@ -12,9 +12,12 @@ 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.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.whispersystems.textsecuregcm.util.AttributeValues.b; import static org.whispersystems.textsecuregcm.util.AttributeValues.n; +import static org.whispersystems.textsecuregcm.util.AttributeValues.s; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -37,15 +40,18 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.function.Predicate; +import java.util.stream.Stream; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Response; import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; @@ -66,6 +72,7 @@ import org.whispersystems.textsecuregcm.subscriptions.StripeManager; import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManager; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.Pair; import org.whispersystems.textsecuregcm.util.SystemMapper; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -84,18 +91,22 @@ class SubscriptionControllerTest { private static final OneTimeDonationConfiguration ONETIME_CONFIG = ConfigHelper.getOneTimeConfig(); private static final SubscriptionManager SUBSCRIPTION_MANAGER = mock(SubscriptionManager.class); private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class); + private static final BraintreeManager BRAINTREE_MANAGER = mock(BraintreeManager.class); private static final PaymentIntent PAYMENT_INTENT = mock(PaymentIntent.class); static { // this behavior is required by the SubscriptionController constructor + List.of(STRIPE_MANAGER, BRAINTREE_MANAGER) + .forEach(manager -> { + when(manager.supportsPaymentMethod(any())) + .thenCallRealMethod(); + }); when(STRIPE_MANAGER.getSupportedCurrencies()) .thenReturn(Set.of("usd", "jpy", "bif")); - when(STRIPE_MANAGER.supportsPaymentMethod(any())) - .thenCallRealMethod(); + when(BRAINTREE_MANAGER.getSupportedCurrencies()) + .thenReturn(Set.of("usd", "jpy")); } - private static final BraintreeManager BRAINTREE_MANAGER = mock(BraintreeManager.class); - private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class); private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class); private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class); @@ -116,13 +127,11 @@ class SubscriptionControllerTest { @BeforeEach void setUp() { - when(STRIPE_MANAGER.getProcessor()).thenReturn(SubscriptionProcessor.STRIPE); - } - - @AfterEach - void tearDown() { - reset(CLOCK, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER, + reset(CLOCK, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, BRAINTREE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR, LEVEL_TRANSLATOR); + + when(STRIPE_MANAGER.getProcessor()).thenReturn(SubscriptionProcessor.STRIPE); + when(BRAINTREE_MANAGER.getProcessor()).thenReturn(SubscriptionProcessor.BRAINTREE); } @Test @@ -431,6 +440,158 @@ class SubscriptionControllerTest { } + @Test + void setSubscriptionLevelMissingProcessorCustomer() { + // set up record + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class))) + .thenReturn(CompletableFuture.completedFuture(record)); + + // set up mocks + when(CLOCK.instant()).thenReturn(Instant.now()); + when(SUBSCRIPTION_MANAGER.get(any(), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + final Response response = RESOURCE_EXTENSION + .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, 5, "usd", "abcd")) + .request() + .put(Entity.json("")); + + assertThat(response.getStatus()).isEqualTo(409); + } + + @Test + void setSubscriptionLevel() { + // set up record + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + final String customerId = "customer"; + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID, + b(new ProcessorCustomer(customerId, SubscriptionProcessor.BRAINTREE).toDynamoBytes()) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class))) + .thenReturn(CompletableFuture.completedFuture(record)); + + // set up mocks + when(CLOCK.instant()).thenReturn(Instant.now()); + when(SUBSCRIPTION_MANAGER.get(any(), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + when(BRAINTREE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(new SubscriptionProcessorManager.SubscriptionId( + "subscription"))); + when(SUBSCRIPTION_MANAGER.subscriptionCreated(any(), any(), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final long level = 5; + final Response response = RESOURCE_EXTENSION + .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, level, "usd", "abcd")) + .request() + .put(Entity.json("")); + + verify(BRAINTREE_MANAGER).createSubscription(eq(customerId), eq("M1"), eq(level), eq(0L)); + verifyNoMoreInteractions(BRAINTREE_MANAGER); + + assertThat(response.getStatus()).isEqualTo(200); + + assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelSuccessResponse.class)) + .extracting(SubscriptionController.SetSubscriptionLevelSuccessResponse::getLevel) + .isEqualTo(level); + } + + @ParameterizedTest + @MethodSource + void setSubscriptionLevelExistingSubscription(final String existingCurrency, final long existingLevel, + final String requestCurrency, final long requestLevel, final boolean expectUpdate) { + + // set up record + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + final String customerId = "customer"; + final String existingSubscriptionId = "existingSubscription"; + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID, + b(new ProcessorCustomer(customerId, SubscriptionProcessor.BRAINTREE).toDynamoBytes()), + SubscriptionManager.KEY_SUBSCRIPTION_ID, s(existingSubscriptionId) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class))) + .thenReturn(CompletableFuture.completedFuture(record)); + + // set up mocks + when(CLOCK.instant()).thenReturn(Instant.now()); + when(SUBSCRIPTION_MANAGER.get(any(), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + final Object subscriptionObj = new Object(); + when(BRAINTREE_MANAGER.getSubscription(any())) + .thenReturn(CompletableFuture.completedFuture(subscriptionObj)); + when(BRAINTREE_MANAGER.getLevelAndCurrencyForSubscription(subscriptionObj)) + .thenReturn(CompletableFuture.completedFuture(new Pair<>(existingLevel, existingCurrency))); + final String updatedSubscriptionId = "updatedSubscriptionId"; + + if (expectUpdate) { + when(BRAINTREE_MANAGER.updateSubscription(any(), any(), anyLong(), anyString())) + .thenReturn(CompletableFuture.completedFuture(new SubscriptionProcessorManager.SubscriptionId( + updatedSubscriptionId))); + when(SUBSCRIPTION_MANAGER.subscriptionLevelChanged(any(), any(), anyLong(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + } + + final String idempotencyKey = "abcd"; + final Response response = RESOURCE_EXTENSION + .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, requestLevel, requestCurrency, + idempotencyKey)) + .request() + .put(Entity.json("")); + + verify(BRAINTREE_MANAGER).getSubscription(any()); + verify(BRAINTREE_MANAGER).getLevelAndCurrencyForSubscription(any()); + + if (expectUpdate) { + verify(BRAINTREE_MANAGER).updateSubscription(any(), any(), eq(requestLevel), eq(idempotencyKey)); + verify(SUBSCRIPTION_MANAGER).subscriptionLevelChanged(any(), any(), eq(requestLevel), eq(updatedSubscriptionId)); + } + + verifyNoMoreInteractions(BRAINTREE_MANAGER); + + assertThat(response.getStatus()).isEqualTo(200); + + assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelSuccessResponse.class)) + .extracting(SubscriptionController.SetSubscriptionLevelSuccessResponse::getLevel) + .isEqualTo(requestLevel); + } + + static Stream setSubscriptionLevelExistingSubscription() { + return Stream.of( + Arguments.of("usd", 5, "usd", 5, false), + Arguments.of("usd", 5, "jpy", 5, true), + Arguments.of("usd", 5, "usd", 15, true), + Arguments.of("usd", 5, "jpy", 15, true) + ); + } + @Test void getSubscriptionConfiguration() { @@ -469,7 +630,7 @@ class SubscriptionControllerTest { List.of(BigDecimal.valueOf(20)))); assertThat(currency.subscription()).isEqualTo( Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15), "35", BigDecimal.valueOf(35))); - assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD")); + assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "PAYPAL")); }); assertThat(currencyMap).extractingByKey("jpy").satisfies(currency -> { @@ -483,7 +644,7 @@ class SubscriptionControllerTest { List.of(BigDecimal.valueOf(2000)))); assertThat(currency.subscription()).isEqualTo( Map.of("5", BigDecimal.valueOf(500), "15", BigDecimal.valueOf(1500), "35", BigDecimal.valueOf(3500))); - assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD")); + assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "PAYPAL")); }); assertThat(currencyMap).extractingByKey("bif").satisfies(currency -> {