mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 19:58:03 +01:00
Check sandbox when storekit transactionId is not found
This commit is contained in:
committed by
ravi-signal
parent
a2ce37fd53
commit
24f8f48a26
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2025 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.assertThatException;
|
||||
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.apple.itunes.storekit.client.APIError;
|
||||
import com.apple.itunes.storekit.client.APIException;
|
||||
import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
|
||||
import com.apple.itunes.storekit.model.Environment;
|
||||
import com.apple.itunes.storekit.model.JWSRenewalInfoDecodedPayload;
|
||||
import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload;
|
||||
import com.apple.itunes.storekit.model.LastTransactionsItem;
|
||||
import com.apple.itunes.storekit.model.Status;
|
||||
import com.apple.itunes.storekit.model.StatusResponse;
|
||||
import com.apple.itunes.storekit.verification.SignedDataVerifier;
|
||||
import com.apple.itunes.storekit.verification.VerificationException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
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.controllers.RateLimitExceededException;
|
||||
|
||||
class AppleAppStoreClientTest {
|
||||
|
||||
private final static String ORIGINAL_TX_ID = "originalTxIdTest";
|
||||
private final static String SIGNED_RENEWAL_INFO = "signedRenewalInfoTest";
|
||||
private final static String SIGNED_TX_INFO = "signedRenewalInfoTest";
|
||||
private final static String PRODUCT_ID = "productIdTest";
|
||||
|
||||
private final AppStoreServerAPIClient productionClient = mock(AppStoreServerAPIClient.class);
|
||||
private final AppStoreServerAPIClient sandboxClient = mock(AppStoreServerAPIClient.class);
|
||||
private final SignedDataVerifier productionSignedDataVerifier = mock(SignedDataVerifier.class);
|
||||
private final SignedDataVerifier sandboxSignedDataVerifier = mock(SignedDataVerifier.class);
|
||||
private AppleAppStoreClient apiWrapper;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
reset(productionClient, productionSignedDataVerifier, sandboxClient, sandboxSignedDataVerifier);
|
||||
apiWrapper = new AppleAppStoreClient(Environment.PRODUCTION, productionSignedDataVerifier, productionClient,
|
||||
sandboxSignedDataVerifier, sandboxClient, null);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = APIError.class, mode = EnumSource.Mode.EXCLUDE, names = "TRANSACTION_ID_NOT_FOUND")
|
||||
public void noFallbackOnOtherErrors(APIError error) throws APIException, IOException {
|
||||
when(productionClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenThrow(new APIException(404, error, "test"));
|
||||
|
||||
assertThatException().isThrownBy(() -> apiWrapper.getAllSubscriptions(ORIGINAL_TX_ID));
|
||||
verifyNoInteractions(sandboxClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallbackOnNoTransactionFound()
|
||||
throws APIException, IOException, SubscriptionInvalidArgumentsException, SubscriptionNotFoundException, RateLimitExceededException {
|
||||
when(productionClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenThrow(new APIException(404, APIError.TRANSACTION_ID_NOT_FOUND, "test"));
|
||||
|
||||
when(sandboxClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenReturn(new StatusResponse().environment(Environment.SANDBOX));
|
||||
|
||||
final StatusResponse allSubscriptions = apiWrapper.getAllSubscriptions(ORIGINAL_TX_ID);
|
||||
|
||||
assertThat(allSubscriptions.getEnvironment()).isEqualTo(Environment.SANDBOX);
|
||||
verify(productionClient).getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{});
|
||||
verify(sandboxClient).getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryEventuallyWorks()
|
||||
throws APIException, IOException, VerificationException, RateLimitExceededException, SubscriptionException {
|
||||
// Should retry up to 3 times
|
||||
when(productionClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenThrow(new APIException(404, APIError.ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE.errorCode(), "test"))
|
||||
.thenThrow(new APIException(404, APIError.ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE.errorCode(), "test"))
|
||||
.thenReturn(new StatusResponse().environment(Environment.PRODUCTION));
|
||||
final StatusResponse statusResponse = apiWrapper.getAllSubscriptions(ORIGINAL_TX_ID);
|
||||
assertThat(statusResponse.getEnvironment()).isEqualTo(Environment.PRODUCTION);
|
||||
verifyNoInteractions(sandboxClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryEventuallyGivesUp() throws APIException, IOException, VerificationException {
|
||||
// Should retry up to 3 times
|
||||
when(productionClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenThrow(new APIException(404, APIError.ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE.errorCode(), "test"));
|
||||
assertThatException()
|
||||
.isThrownBy(() -> apiWrapper.getAllSubscriptions(ORIGINAL_TX_ID))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.withRootCauseInstanceOf(APIException.class);
|
||||
|
||||
verify(productionClient, times(3)).getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{});
|
||||
verifyNoInteractions(sandboxClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sandboxDoesRetries()
|
||||
throws APIException, IOException, SubscriptionInvalidArgumentsException, SubscriptionNotFoundException, RateLimitExceededException {
|
||||
when(productionClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenThrow(new APIException(404, APIError.TRANSACTION_ID_NOT_FOUND, "test"));
|
||||
|
||||
when(sandboxClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenThrow(new APIException(404, APIError.ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE.errorCode(), "test"))
|
||||
.thenThrow(new APIException(404, APIError.ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE.errorCode(), "test"))
|
||||
.thenReturn(new StatusResponse().environment(Environment.SANDBOX));
|
||||
|
||||
final StatusResponse statusResponse = apiWrapper.getAllSubscriptions(ORIGINAL_TX_ID);
|
||||
assertThat(statusResponse.getEnvironment()).isEqualTo(Environment.SANDBOX);
|
||||
|
||||
verify(productionClient, times(3))
|
||||
.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{});
|
||||
verify(sandboxClient, times(3))
|
||||
.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = Environment.class, mode = EnumSource.Mode.INCLUDE, names = {"SANDBOX", "PRODUCTION"})
|
||||
public void verifySignatureTest(Environment environment) throws VerificationException {
|
||||
final SignedDataVerifier expectedVerifier, unexpectedVerifier;
|
||||
if (environment.equals(Environment.SANDBOX)) {
|
||||
expectedVerifier = sandboxSignedDataVerifier;
|
||||
unexpectedVerifier = productionSignedDataVerifier;
|
||||
} else {
|
||||
expectedVerifier = productionSignedDataVerifier;
|
||||
unexpectedVerifier = sandboxSignedDataVerifier;
|
||||
}
|
||||
|
||||
when(expectedVerifier.verifyAndDecodeTransaction(SIGNED_TX_INFO))
|
||||
.thenReturn(new JWSTransactionDecodedPayload().productId(PRODUCT_ID));
|
||||
when(expectedVerifier.verifyAndDecodeRenewalInfo(SIGNED_RENEWAL_INFO))
|
||||
.thenReturn(new JWSRenewalInfoDecodedPayload());
|
||||
|
||||
apiWrapper.verify(environment, new LastTransactionsItem()
|
||||
.originalTransactionId(ORIGINAL_TX_ID)
|
||||
.status(Status.ACTIVE)
|
||||
.signedRenewalInfo(SIGNED_RENEWAL_INFO)
|
||||
.signedTransactionInfo(SIGNED_TX_INFO));
|
||||
|
||||
verify(expectedVerifier).verifyAndDecodeTransaction(SIGNED_TX_INFO);
|
||||
verify(expectedVerifier).verifyAndDecodeRenewalInfo(SIGNED_RENEWAL_INFO);
|
||||
verifyNoInteractions(unexpectedVerifier);
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,16 @@
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
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.when;
|
||||
|
||||
import com.apple.itunes.storekit.client.APIError;
|
||||
import com.apple.itunes.storekit.client.APIException;
|
||||
import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
|
||||
import com.apple.itunes.storekit.model.AutoRenewStatus;
|
||||
import com.apple.itunes.storekit.model.Environment;
|
||||
import com.apple.itunes.storekit.model.JWSRenewalInfoDecodedPayload;
|
||||
import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload;
|
||||
import com.apple.itunes.storekit.model.LastTransactionsItem;
|
||||
@@ -28,7 +25,6 @@ import com.apple.itunes.storekit.model.SubscriptionGroupIdentifierItem;
|
||||
import com.apple.itunes.storekit.verification.SignedDataVerifier;
|
||||
import com.apple.itunes.storekit.verification.VerificationException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -57,8 +53,10 @@ class AppleAppStoreManagerTest {
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
reset(apiClient, signedDataVerifier);
|
||||
appleAppStoreManager = new AppleAppStoreManager(apiClient, signedDataVerifier,
|
||||
SUBSCRIPTION_GROUP_ID, Map.of(PRODUCT_ID, LEVEL), null);
|
||||
appleAppStoreManager = new AppleAppStoreManager(new AppleAppStoreClient(Environment.PRODUCTION,
|
||||
signedDataVerifier, apiClient,
|
||||
signedDataVerifier, apiClient, null),
|
||||
SUBSCRIPTION_GROUP_ID, Map.of(PRODUCT_ID, LEVEL));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -126,7 +124,8 @@ class AppleAppStoreManagerTest {
|
||||
.status(Status.ACTIVE)
|
||||
.signedRenewalInfo(SIGNED_RENEWAL_INFO)
|
||||
.signedTransactionInfo(product + "_signed_tx"))
|
||||
.toList()))));
|
||||
.toList())))
|
||||
.environment(Environment.PRODUCTION));
|
||||
when(signedDataVerifier.verifyAndDecodeRenewalInfo(SIGNED_RENEWAL_INFO))
|
||||
.thenReturn(new JWSRenewalInfoDecodedPayload()
|
||||
.autoRenewStatus(AutoRenewStatus.ON));
|
||||
@@ -148,39 +147,6 @@ class AppleAppStoreManagerTest {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryEventuallyWorks() throws APIException, IOException, VerificationException, RateLimitExceededException, SubscriptionException {
|
||||
// Should retry up to 3 times
|
||||
when(apiClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenThrow(new APIException(404, APIError.ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE.errorCode(), "test"))
|
||||
.thenThrow(new APIException(404, APIError.ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE.errorCode(), "test"))
|
||||
.thenReturn(new StatusResponse().data(List.of(new SubscriptionGroupIdentifierItem()
|
||||
.subscriptionGroupIdentifier(SUBSCRIPTION_GROUP_ID)
|
||||
.addLastTransactionsItem(new LastTransactionsItem()
|
||||
.originalTransactionId(ORIGINAL_TX_ID)
|
||||
.status(Status.ACTIVE)
|
||||
.signedRenewalInfo(SIGNED_RENEWAL_INFO)
|
||||
.signedTransactionInfo(SIGNED_TX_INFO)))));
|
||||
mockDecode(AutoRenewStatus.ON);
|
||||
final SubscriptionInformation info = appleAppStoreManager.getSubscriptionInformation(ORIGINAL_TX_ID);
|
||||
assertThat(info.status()).isEqualTo(SubscriptionStatus.ACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryEventuallyGivesUp() throws APIException, IOException, VerificationException {
|
||||
// Should retry up to 3 times
|
||||
when(apiClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenThrow(new APIException(404, APIError.ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE.errorCode(), "test"));
|
||||
mockDecode(AutoRenewStatus.ON);
|
||||
assertThatException()
|
||||
.isThrownBy(() -> appleAppStoreManager.getSubscriptionInformation(ORIGINAL_TX_ID))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.withRootCauseInstanceOf(APIException.class);
|
||||
|
||||
verify(apiClient, times(3)).getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelRenewalDisabled() throws APIException, VerificationException, IOException {
|
||||
mockSubscription(Status.ACTIVE, AutoRenewStatus.OFF);
|
||||
@@ -205,13 +171,15 @@ class AppleAppStoreManagerTest {
|
||||
private void mockSubscription(final Status status, final AutoRenewStatus autoRenewStatus)
|
||||
throws APIException, IOException, VerificationException {
|
||||
when(apiClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{}))
|
||||
.thenReturn(new StatusResponse().data(List.of(new SubscriptionGroupIdentifierItem()
|
||||
.thenReturn(new StatusResponse()
|
||||
.data(List.of(new SubscriptionGroupIdentifierItem()
|
||||
.subscriptionGroupIdentifier(SUBSCRIPTION_GROUP_ID)
|
||||
.addLastTransactionsItem(new LastTransactionsItem()
|
||||
.originalTransactionId(ORIGINAL_TX_ID)
|
||||
.status(status)
|
||||
.signedRenewalInfo(SIGNED_RENEWAL_INFO)
|
||||
.signedTransactionInfo(SIGNED_TX_INFO)))));
|
||||
.signedTransactionInfo(SIGNED_TX_INFO))))
|
||||
.environment(Environment.PRODUCTION));
|
||||
mockDecode(autoRenewStatus);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user