diff --git a/src/main/java/com/stripe/StripeClient.java b/src/main/java/com/stripe/StripeClient.java
index 8031aa125ef..77723ece24c 100644
--- a/src/main/java/com/stripe/StripeClient.java
+++ b/src/main/java/com/stripe/StripeClient.java
@@ -41,6 +41,73 @@ protected StripeResponseGetter getResponseGetter() {
return responseGetter;
}
+ /** Gets the current StripeContext from the client's configuration. Used in unit testing. */
+ protected String getContext() {
+ // TODO(major): add getOptions to the StripeResponseGetter interface? that would simplify this
+ if (!(responseGetter instanceof LiveStripeResponseGetter)) {
+ return null;
+ }
+
+ LiveStripeResponseGetter liveGetter = (LiveStripeResponseGetter) responseGetter;
+ StripeResponseGetterOptions options = liveGetter.getOptions();
+
+ return options.getStripeContext();
+ }
+
+ /**
+ * Creates a new StripeClient with the same configuration as this client but with a custom
+ * StripeContext. This method is useful for creating thread-safe clients with different contexts,
+ * such as when processing webhooks in parallel where each webhook has its own context.
+ *
+ *
The new client will share the same configuration (API key, timeouts, proxy settings, etc.)
+ * and HTTP client as this client, but will have the specified context. This allows for efficient
+ * parallel processing without reinitializing HTTP connections.
+ *
+ * @param context the custom stripe_context to use for the new client
+ * @return a new StripeClient with the custom context
+ * @throws IllegalStateException if this client doesn't use a LiveStripeResponseGetter
+ */
+ public StripeClient withStripeContext(StripeContext context) {
+ // Convert StripeContext to String
+ String contextString = (context == null) ? null : context.toString();
+
+ StripeResponseGetter responseGetter = this.getResponseGetter();
+
+ // We can only create a new client for LiveStripeResponseGetter
+ if (!(responseGetter instanceof LiveStripeResponseGetter)) {
+ throw new IllegalStateException(
+ "Cannot create a client with custom context for non-Live response getters");
+ }
+
+ LiveStripeResponseGetter liveGetter = (LiveStripeResponseGetter) responseGetter;
+
+ // Create a new LiveStripeResponseGetter with updated context, reusing the HTTP client
+ LiveStripeResponseGetter newResponseGetter =
+ liveGetter.withNewOptions(
+ options -> {
+ ClientStripeResponseGetterOptions existingOptions =
+ (ClientStripeResponseGetterOptions) options;
+
+ return new ClientStripeResponseGetterOptions(
+ existingOptions.getAuthenticator(),
+ existingOptions.getClientId(),
+ existingOptions.getConnectTimeout(),
+ existingOptions.getReadTimeout(),
+ existingOptions.getMaxNetworkRetries(),
+ existingOptions.getConnectionProxy(),
+ existingOptions.getProxyCredential(),
+ existingOptions.getApiBase(),
+ existingOptions.getFilesBase(),
+ existingOptions.getConnectBase(),
+ existingOptions.getMeterEventsBase(),
+ existingOptions.getStripeAccount(),
+ contextString);
+ });
+
+ // Create and return a new StripeClient with the new response getter
+ return new StripeClient(newResponseGetter);
+ }
+
/**
* Returns an StripeEvent instance using the provided JSON payload. Throws a JsonSyntaxException
* if the payload is not valid JSON, and a SignatureVerificationException if the signature
@@ -1412,4 +1479,9 @@ public StripeResponse rawRequest(
public StripeObject deserialize(String rawJson, ApiMode apiMode) throws StripeException {
return StripeObject.deserializeStripeObject(rawJson, this.getResponseGetter(), apiMode);
}
+
+ public StripeEventNotificationHandler notificationHandler(
+ String webhookSecret, StripeEventNotificationHandler.FallbackCallback fallbackCallback) {
+ return new StripeEventNotificationHandler(webhookSecret, this, fallbackCallback);
+ }
}
diff --git a/src/main/java/com/stripe/StripeContext.java b/src/main/java/com/stripe/StripeContext.java
index cd7b15b19e8..a86572595ed 100644
--- a/src/main/java/com/stripe/StripeContext.java
+++ b/src/main/java/com/stripe/StripeContext.java
@@ -64,8 +64,8 @@ public StripeContext pop() {
/**
* Converts the context to a string by joining segments with '/'.
*
- * @return string representation of the context segments joined by '/', `null` if there are no
- * segments (useful for clearing context)
+ * @return string representation of the context segments joined by '/'. If there are no segments,
+ * returns an empty string (useful for clearing context).
*/
@Override
public String toString() {
diff --git a/src/main/java/com/stripe/StripeEventNotificationHandler.java b/src/main/java/com/stripe/StripeEventNotificationHandler.java
new file mode 100644
index 00000000000..e0843146789
--- /dev/null
+++ b/src/main/java/com/stripe/StripeEventNotificationHandler.java
@@ -0,0 +1,541 @@
+package com.stripe;
+
+// event-notification-class-imports: The beginning of the section generated from our OpenAPI spec
+// - hack because we can't format java files whose imports aren't a single contiguous block
+// - so _any_ imports in this file have to come from codegen
+// - as do these comments, explaining the whole thing
+import com.stripe.events.V1BillingMeterErrorReportTriggeredEventNotification;
+import com.stripe.events.V1BillingMeterNoMeterFoundEventNotification;
+import com.stripe.events.V2CoreAccountClosedEventNotification;
+import com.stripe.events.V2CoreAccountCreatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingConfigurationCustomerCapabilityStatusUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingConfigurationCustomerUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingConfigurationMerchantCapabilityStatusUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingConfigurationMerchantUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingConfigurationRecipientCapabilityStatusUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingConfigurationRecipientUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingConfigurationStorerCapabilityStatusUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingConfigurationStorerUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingDefaultsUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingIdentityUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountIncludingRequirementsUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountLinkReturnedEventNotification;
+import com.stripe.events.V2CoreAccountPersonCreatedEventNotification;
+import com.stripe.events.V2CoreAccountPersonDeletedEventNotification;
+import com.stripe.events.V2CoreAccountPersonUpdatedEventNotification;
+import com.stripe.events.V2CoreAccountUpdatedEventNotification;
+import com.stripe.events.V2CoreEventDestinationPingEventNotification;
+import com.stripe.events.V2CoreHealthEventGenerationFailureResolvedEventNotification;
+import com.stripe.events.V2MoneyManagementAdjustmentCreatedEventNotification;
+import com.stripe.events.V2MoneyManagementFinancialAccountCreatedEventNotification;
+import com.stripe.events.V2MoneyManagementFinancialAccountUpdatedEventNotification;
+import com.stripe.events.V2MoneyManagementFinancialAddressActivatedEventNotification;
+import com.stripe.events.V2MoneyManagementFinancialAddressFailedEventNotification;
+import com.stripe.events.V2MoneyManagementInboundTransferAvailableEventNotification;
+import com.stripe.events.V2MoneyManagementInboundTransferBankDebitFailedEventNotification;
+import com.stripe.events.V2MoneyManagementInboundTransferBankDebitProcessingEventNotification;
+import com.stripe.events.V2MoneyManagementInboundTransferBankDebitQueuedEventNotification;
+import com.stripe.events.V2MoneyManagementInboundTransferBankDebitReturnedEventNotification;
+import com.stripe.events.V2MoneyManagementInboundTransferBankDebitSucceededEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundPaymentCanceledEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundPaymentCreatedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundPaymentFailedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundPaymentPostedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundPaymentReturnedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundPaymentUpdatedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundTransferCanceledEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundTransferCreatedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundTransferFailedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundTransferPostedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundTransferReturnedEventNotification;
+import com.stripe.events.V2MoneyManagementOutboundTransferUpdatedEventNotification;
+import com.stripe.events.V2MoneyManagementPayoutMethodUpdatedEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedCreditAvailableEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedCreditFailedEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedCreditReturnedEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedCreditSucceededEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedDebitCanceledEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedDebitFailedEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedDebitPendingEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedDebitSucceededEventNotification;
+import com.stripe.events.V2MoneyManagementReceivedDebitUpdatedEventNotification;
+import com.stripe.events.V2MoneyManagementTransactionCreatedEventNotification;
+import com.stripe.events.V2MoneyManagementTransactionUpdatedEventNotification;
+import com.stripe.exception.SignatureVerificationException;
+import com.stripe.model.v2.core.EventNotification;
+import java.util.HashMap;
+
+// event-notification-class-imports: The end of the section generated from our OpenAPI spec
+
+public class StripeEventNotificationHandler {
+ /**
+ * Functional interface for callback functions. It describes the signature of the functions you'll
+ * register on the StripeEventRouter to process incoming event notifications.
+ */
+ @FunctionalInterface
+ public interface Callback {
+ // this is an internal-facing method name that dictates how we call the stored method
+ void process(T event, StripeClient client);
+ }
+
+ /**
+ * Functional interface for handling otherwise unhandled events. It's similar to {@link Callback},
+ * but includes additional information about the unhandled event to help debug it.
+ */
+ @FunctionalInterface
+ public interface FallbackCallback {
+ // this is an internal-facing method name that dictates how we call the stored method
+ void process(
+ EventNotification event, StripeClient client, UnhandledNotificationDetails details);
+ }
+
+ /**
+ * Information about an unhandled event notification to make it easier to respond (and potentially
+ * update your integration).
+ */
+ public static class UnhandledNotificationDetails {
+ private boolean isKnownEventType;
+
+ private UnhandledNotificationDetails(boolean isKnownEventType) {
+ this.isKnownEventType = isKnownEventType;
+ }
+
+ /**
+ * If true, the unhandled event's type is known to the SDK (i.e., it was successfully
+ * deserialized into a specific `EventNotification` subclass).
+ */
+ public boolean isKnownEventType() {
+ return isKnownEventType;
+ }
+ }
+
+ private boolean hasHandledEvent = false;
+
+ private final String webhookSecret;
+ private final StripeClient client;
+ private final FallbackCallback fallbackCallback;
+ private final HashMap> registeredHandlers =
+ new HashMap<>();
+
+ public StripeEventNotificationHandler(
+ String webhookSecret, StripeClient client, FallbackCallback fallbackCallback) {
+ this.webhookSecret = webhookSecret;
+ this.client = client;
+ this.fallbackCallback = fallbackCallback;
+ }
+
+ private StripeEventNotificationHandler register(
+ String eventType, Callback handler) {
+ if (hasHandledEvent) {
+ throw new IllegalStateException("Cannot register handlers after handling an event");
+ }
+
+ if (this.registeredHandlers.containsKey(eventType)) {
+ throw new IllegalArgumentException("Handler already registered for event type: " + eventType);
+ }
+ this.registeredHandlers.put(eventType, handler);
+ return this;
+ }
+
+ /**
+ * Handle an incoming webhook event notification.
+ *
+ * @param webhookBody the incoming webhook body
+ * @param sigHeader the incoming webhook signature header
+ * @throws SignatureVerificationException if the validation of the webhook signature fails
+ * @throws IllegalArgumentException if no handler is registered for the event type
+ */
+ @SuppressWarnings("unchecked")
+ public void handle(String webhookBody, String sigHeader) throws SignatureVerificationException {
+ // setting this naiively isn't technically thread-safe, but we expect the all callbacks to be
+ // registered syncronously on startup, so this should be fine
+ hasHandledEvent = true;
+
+ EventNotification eventNotification =
+ this.client.parseEventNotification(webhookBody, sigHeader, this.webhookSecret);
+
+ Callback extends EventNotification> handler =
+ registeredHandlers.get(eventNotification.getType());
+
+ // Create a new client with the event's context for thread-safe processing
+ StripeClient eventClient = this.client.withStripeContext(eventNotification.context);
+
+ if (handler == null) {
+ boolean isKnownEventType =
+ !(eventNotification instanceof com.stripe.events.UnknownEventNotification);
+ UnhandledNotificationDetails details = new UnhandledNotificationDetails(isKnownEventType);
+
+ this.fallbackCallback.process(eventNotification, eventClient, details);
+ } else {
+ // this is technically unsafe but we control the registration API so should be ok
+ ((Callback) handler).process(eventNotification, eventClient);
+ }
+ }
+
+ // notification-handler-methods: The beginning of the section generated from our OpenAPI spec
+ public StripeEventNotificationHandler onV1BillingMeterErrorReportTriggered(
+ Callback callback) {
+ this.register("v1.billing.meter.error_report_triggered", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV1BillingMeterNoMeterFound(
+ Callback callback) {
+ this.register("v1.billing.meter.no_meter_found", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountClosed(
+ Callback callback) {
+ this.register("v2.core.account.closed", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountCreated(
+ Callback callback) {
+ this.register("v2.core.account.created", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountUpdated(
+ Callback callback) {
+ this.register("v2.core.account.updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler
+ onV2CoreAccountIncludingConfigurationCustomerCapabilityStatusUpdated(
+ Callback<
+ V2CoreAccountIncludingConfigurationCustomerCapabilityStatusUpdatedEventNotification>
+ callback) {
+ this.register("v2.core.account[configuration.customer].capability_status_updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountIncludingConfigurationCustomerUpdated(
+ Callback callback) {
+ this.register("v2.core.account[configuration.customer].updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler
+ onV2CoreAccountIncludingConfigurationMerchantCapabilityStatusUpdated(
+ Callback<
+ V2CoreAccountIncludingConfigurationMerchantCapabilityStatusUpdatedEventNotification>
+ callback) {
+ this.register("v2.core.account[configuration.merchant].capability_status_updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountIncludingConfigurationMerchantUpdated(
+ Callback callback) {
+ this.register("v2.core.account[configuration.merchant].updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler
+ onV2CoreAccountIncludingConfigurationRecipientCapabilityStatusUpdated(
+ Callback<
+ V2CoreAccountIncludingConfigurationRecipientCapabilityStatusUpdatedEventNotification>
+ callback) {
+ this.register("v2.core.account[configuration.recipient].capability_status_updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountIncludingConfigurationRecipientUpdated(
+ Callback callback) {
+ this.register("v2.core.account[configuration.recipient].updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler
+ onV2CoreAccountIncludingConfigurationStorerCapabilityStatusUpdated(
+ Callback<
+ V2CoreAccountIncludingConfigurationStorerCapabilityStatusUpdatedEventNotification>
+ callback) {
+ this.register("v2.core.account[configuration.storer].capability_status_updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountIncludingConfigurationStorerUpdated(
+ Callback callback) {
+ this.register("v2.core.account[configuration.storer].updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountIncludingDefaultsUpdated(
+ Callback callback) {
+ this.register("v2.core.account[defaults].updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountIncludingIdentityUpdated(
+ Callback callback) {
+ this.register("v2.core.account[identity].updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountIncludingRequirementsUpdated(
+ Callback callback) {
+ this.register("v2.core.account[requirements].updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountLinkReturned(
+ Callback callback) {
+ this.register("v2.core.account_link.returned", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountPersonCreated(
+ Callback callback) {
+ this.register("v2.core.account_person.created", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountPersonDeleted(
+ Callback callback) {
+ this.register("v2.core.account_person.deleted", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreAccountPersonUpdated(
+ Callback callback) {
+ this.register("v2.core.account_person.updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreEventDestinationPing(
+ Callback callback) {
+ this.register("v2.core.event_destination.ping", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2CoreHealthEventGenerationFailureResolved(
+ Callback callback) {
+ this.register("v2.core.health.event_generation_failure.resolved", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementAdjustmentCreated(
+ Callback callback) {
+ this.register("v2.money_management.adjustment.created", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementFinancialAccountCreated(
+ Callback callback) {
+ this.register("v2.money_management.financial_account.created", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementFinancialAccountUpdated(
+ Callback callback) {
+ this.register("v2.money_management.financial_account.updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementFinancialAddressActivated(
+ Callback callback) {
+ this.register("v2.money_management.financial_address.activated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementFinancialAddressFailed(
+ Callback callback) {
+ this.register("v2.money_management.financial_address.failed", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementInboundTransferAvailable(
+ Callback callback) {
+ this.register("v2.money_management.inbound_transfer.available", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementInboundTransferBankDebitFailed(
+ Callback callback) {
+ this.register("v2.money_management.inbound_transfer.bank_debit_failed", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementInboundTransferBankDebitProcessing(
+ Callback callback) {
+ this.register("v2.money_management.inbound_transfer.bank_debit_processing", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementInboundTransferBankDebitQueued(
+ Callback callback) {
+ this.register("v2.money_management.inbound_transfer.bank_debit_queued", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementInboundTransferBankDebitReturned(
+ Callback callback) {
+ this.register("v2.money_management.inbound_transfer.bank_debit_returned", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementInboundTransferBankDebitSucceeded(
+ Callback callback) {
+ this.register("v2.money_management.inbound_transfer.bank_debit_succeeded", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundPaymentCanceled(
+ Callback callback) {
+ this.register("v2.money_management.outbound_payment.canceled", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundPaymentCreated(
+ Callback callback) {
+ this.register("v2.money_management.outbound_payment.created", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundPaymentFailed(
+ Callback callback) {
+ this.register("v2.money_management.outbound_payment.failed", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundPaymentPosted(
+ Callback callback) {
+ this.register("v2.money_management.outbound_payment.posted", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundPaymentReturned(
+ Callback callback) {
+ this.register("v2.money_management.outbound_payment.returned", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundPaymentUpdated(
+ Callback callback) {
+ this.register("v2.money_management.outbound_payment.updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundTransferCanceled(
+ Callback callback) {
+ this.register("v2.money_management.outbound_transfer.canceled", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundTransferCreated(
+ Callback callback) {
+ this.register("v2.money_management.outbound_transfer.created", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundTransferFailed(
+ Callback callback) {
+ this.register("v2.money_management.outbound_transfer.failed", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundTransferPosted(
+ Callback callback) {
+ this.register("v2.money_management.outbound_transfer.posted", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundTransferReturned(
+ Callback callback) {
+ this.register("v2.money_management.outbound_transfer.returned", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementOutboundTransferUpdated(
+ Callback callback) {
+ this.register("v2.money_management.outbound_transfer.updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementPayoutMethodUpdated(
+ Callback callback) {
+ this.register("v2.money_management.payout_method.updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedCreditAvailable(
+ Callback callback) {
+ this.register("v2.money_management.received_credit.available", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedCreditFailed(
+ Callback callback) {
+ this.register("v2.money_management.received_credit.failed", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedCreditReturned(
+ Callback callback) {
+ this.register("v2.money_management.received_credit.returned", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedCreditSucceeded(
+ Callback callback) {
+ this.register("v2.money_management.received_credit.succeeded", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedDebitCanceled(
+ Callback callback) {
+ this.register("v2.money_management.received_debit.canceled", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedDebitFailed(
+ Callback callback) {
+ this.register("v2.money_management.received_debit.failed", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedDebitPending(
+ Callback callback) {
+ this.register("v2.money_management.received_debit.pending", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedDebitSucceeded(
+ Callback callback) {
+ this.register("v2.money_management.received_debit.succeeded", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementReceivedDebitUpdated(
+ Callback callback) {
+ this.register("v2.money_management.received_debit.updated", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementTransactionCreated(
+ Callback callback) {
+ this.register("v2.money_management.transaction.created", callback);
+ return this;
+ }
+
+ public StripeEventNotificationHandler onV2MoneyManagementTransactionUpdated(
+ Callback callback) {
+ this.register("v2.money_management.transaction.updated", callback);
+ return this;
+ }
+ // notification-handler-methods: The end of the section generated from our OpenAPI spec
+
+ /**
+ * Get a sorted list of all registered event types.
+ *
+ * @return A sorted list of event type strings
+ */
+ public java.util.List getRegisteredEventTypes() {
+ java.util.List eventTypes = new java.util.ArrayList<>(this.registeredHandlers.keySet());
+ java.util.Collections.sort(eventTypes);
+ return eventTypes;
+ }
+}
diff --git a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java
index d8fdc544cf7..742d88c33e4 100644
--- a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java
+++ b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java
@@ -76,6 +76,27 @@ public LiveStripeResponseGetter(StripeResponseGetterOptions options, HttpClient
this.httpClient = (httpClient != null) ? httpClient : buildDefaultHttpClient();
}
+ /**
+ * Creates a new LiveStripeResponseGetter with the same configuration and HTTP client as this
+ * instance, but with a different stripe_context. This allows for efficient cloning when you need
+ * to make requests with different contexts (e.g., webhook processing) without reinitializing HTTP
+ * connections.
+ *
+ * @param contextCreator a function that takes the existing options and returns new options with
+ * the desired context
+ * @return a new LiveStripeResponseGetter with the updated options and the same HTTP client
+ */
+ public LiveStripeResponseGetter withNewOptions(
+ java.util.function.Function
+ contextCreator) {
+ StripeResponseGetterOptions newOptions = contextCreator.apply(this.options);
+ return new LiveStripeResponseGetter(newOptions, this.httpClient);
+ }
+
+ public StripeResponseGetterOptions getOptions() {
+ return this.options;
+ }
+
private StripeRequest toStripeRequest(ApiRequest apiRequest, RequestOptions mergedOptions)
throws StripeException {
String fullUrl = fullUrl(apiRequest);
diff --git a/src/test/java/com/stripe/StripeEventNotificationHandlerTest.java b/src/test/java/com/stripe/StripeEventNotificationHandlerTest.java
new file mode 100644
index 00000000000..eaa77ac4bd2
--- /dev/null
+++ b/src/test/java/com/stripe/StripeEventNotificationHandlerTest.java
@@ -0,0 +1,517 @@
+package com.stripe;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.stripe.events.UnknownEventNotification;
+import com.stripe.events.V1BillingMeterErrorReportTriggeredEventNotification;
+import com.stripe.events.V2CoreAccountCreatedEventNotification;
+import com.stripe.exception.SignatureVerificationException;
+import com.stripe.model.v2.core.EventNotification;
+import com.stripe.net.Webhook;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class StripeEventNotificationHandlerTest {
+ private static final String DUMMY_WEBHOOK_SECRET = "whsec_test_secret";
+
+ private StripeClient stripeClient;
+ private StripeEventNotificationHandler.FallbackCallback fallbackCallback;
+ private StripeEventNotificationHandler eventNotificationHandler;
+
+ private String v1BillingMeterPayload;
+ private String v2AccountCreatedPayload;
+ private String unknownEventPayload;
+
+ @BeforeEach
+ public void setUp() {
+ // Create a StripeClient with context
+ stripeClient =
+ StripeClient.builder()
+ .setApiKey("sk_test_1234")
+ .setStripeContext("original_context_123")
+ .build();
+
+ // Create mock handler for unhandled events
+ fallbackCallback = mock(StripeEventNotificationHandler.FallbackCallback.class);
+
+ // Create event router
+ eventNotificationHandler =
+ new StripeEventNotificationHandler(DUMMY_WEBHOOK_SECRET, stripeClient, fallbackCallback);
+
+ // Set up test payloads
+ v1BillingMeterPayload =
+ "{"
+ + "\"id\": \"evt_123\","
+ + "\"object\": \"v2.core.event\","
+ + "\"type\": \"v1.billing.meter.error_report_triggered\","
+ + "\"livemode\": false,"
+ + "\"created\": \"2022-02-15T00:27:45.330Z\","
+ + "\"context\": \"event_context_456\","
+ + "\"related_object\": {"
+ + "\"id\": \"mtr_123\","
+ + "\"type\": \"billing.meter\","
+ + "\"url\": \"/v1/billing/meters/mtr_123\""
+ + "}"
+ + "}";
+
+ v2AccountCreatedPayload =
+ "{"
+ + "\"id\": \"evt_789\","
+ + "\"object\": \"v2.core.event\","
+ + "\"type\": \"v2.core.account.created\","
+ + "\"livemode\": false,"
+ + "\"created\": \"2022-02-15T00:27:45.330Z\","
+ + "\"context\": null,"
+ + "\"related_object\": {"
+ + "\"id\": \"acct_abc\","
+ + "\"type\": \"account\","
+ + "\"url\": \"/v2/core/accounts/acct_abc\""
+ + "}"
+ + "}";
+
+ unknownEventPayload =
+ "{"
+ + "\"id\": \"evt_unknown\","
+ + "\"object\": \"v2.core.event\","
+ + "\"type\": \"llama.created\","
+ + "\"livemode\": false,"
+ + "\"created\": \"2022-02-15T00:27:45.330Z\","
+ + "\"context\": \"event_context_unknown\","
+ + "\"related_object\": {"
+ + "\"id\": \"llama_123\","
+ + "\"type\": \"llama\","
+ + "\"url\": \"/v1/llamas/llama_123\""
+ + "}"
+ + "}";
+ }
+
+ private String generateSigHeader(String payload)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ Map options = new HashMap<>();
+ options.put("payload", payload);
+ options.put("secret", DUMMY_WEBHOOK_SECRET);
+ return generateSigHeader(options);
+ }
+
+ private String generateSigHeader(Map options)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ final long timestamp =
+ (options.get("timestamp") != null)
+ ? ((Long) options.get("timestamp")).longValue()
+ : Webhook.Util.getTimeNow();
+ final String payload = (String) options.get("payload");
+ final String secret = (String) options.get("secret");
+ final String scheme =
+ (options.get("scheme") != null)
+ ? (String) options.get("scheme")
+ : Webhook.Signature.EXPECTED_SCHEME;
+ String signature = (String) options.get("signature");
+
+ if (signature == null) {
+ final String payloadToSign = String.format("%d.%s", timestamp, payload);
+ signature = Webhook.Util.computeHmacSha256(secret, payloadToSign);
+ }
+
+ return String.format("t=%d,%s=%s", timestamp, scheme, signature);
+ }
+
+ @Test
+ public void testRoutesEventToRegisteredHandler()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that a registered event type is routed to the correct handler
+ @SuppressWarnings("unchecked")
+ StripeEventNotificationHandler.Callback
+ handler = mock(StripeEventNotificationHandler.Callback.class);
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader);
+
+ verify(handler, times(1))
+ .process(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+ verify(fallbackCallback, never())
+ .process(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testRoutesDifferentEventsToCorrectHandlers()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that different event types route to their respective handlers
+ StripeEventNotificationHandler.Callback
+ billingHandler = mock(StripeEventNotificationHandler.Callback.class);
+ StripeEventNotificationHandler.Callback accountHandler =
+ mock(StripeEventNotificationHandler.Callback.class);
+
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(billingHandler);
+ eventNotificationHandler.onV2CoreAccountCreated(accountHandler);
+
+ String sigHeader1 = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader1);
+
+ String sigHeader2 = generateSigHeader(v2AccountCreatedPayload);
+ eventNotificationHandler.handle(v2AccountCreatedPayload, sigHeader2);
+
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader1);
+
+ verify(billingHandler, times(2))
+ .process(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+ verify(accountHandler, times(1))
+ .process(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+ verify(fallbackCallback, never())
+ .process(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any());
+ }
+
+ @Test
+ public void testHandlerReceivesCorrectRuntimeType()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that handlers receive the correctly typed event notification
+ AtomicReference receivedEvent = new AtomicReference<>();
+ AtomicReference receivedClient = new AtomicReference<>();
+
+ StripeEventNotificationHandler.Callback
+ handler =
+ (event, client) -> {
+ receivedEvent.set(event);
+ receivedClient.set(client);
+ };
+
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader);
+
+ assertInstanceOf(
+ V1BillingMeterErrorReportTriggeredEventNotification.class, receivedEvent.get());
+ V1BillingMeterErrorReportTriggeredEventNotification notification =
+ (V1BillingMeterErrorReportTriggeredEventNotification) receivedEvent.get();
+ assertEquals("v1.billing.meter.error_report_triggered", notification.getType());
+ assertEquals("evt_123", notification.getId());
+ assertEquals("mtr_123", notification.getRelatedObject().getId());
+ assertNotNull(receivedClient.get());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testCannotRegisterHandlerAfterHandling()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that registering handlers after handle() raises IllegalStateException
+ StripeEventNotificationHandler.Callback
+ handler = mock(StripeEventNotificationHandler.Callback.class);
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader);
+
+ IllegalStateException exception =
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ eventNotificationHandler.onV2CoreAccountCreated(
+ mock(StripeEventNotificationHandler.Callback.class)));
+
+ assertTrue(exception.getMessage().contains("Cannot register handlers after handling an event"));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testCannotRegisterDuplicateHandler() {
+ // Test that registering the same event type twice raises IllegalArgumentException
+ StripeEventNotificationHandler.Callback
+ handler1 = mock(StripeEventNotificationHandler.Callback.class);
+ StripeEventNotificationHandler.Callback
+ handler2 = mock(StripeEventNotificationHandler.Callback.class);
+
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler1);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler2));
+
+ assertTrue(
+ exception
+ .getMessage()
+ .contains(
+ "Handler already registered for event type: v1.billing.meter.error_report_triggered"));
+ }
+
+ @Test
+ public void testHandlerUsesEventStripeContext()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that the handler receives a client with stripe_context from the event
+ AtomicReference receivedContext = new AtomicReference<>();
+
+ StripeEventNotificationHandler.Callback
+ handler =
+ (event, client) -> {
+ receivedContext.set(client.getContext());
+ };
+
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+
+ assertEquals("original_context_123", stripeClient.getContext());
+
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader);
+
+ assertEquals("event_context_456", receivedContext.get());
+ }
+
+ @Test
+ public void testStripeContextRestoredAfterHandlerSuccess()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that the original stripe_context is restored after successful handler execution
+ StripeEventNotificationHandler.Callback
+ handler =
+ (event, client) -> {
+ assertEquals("event_context_456", client.getContext());
+ };
+
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+
+ assertEquals("original_context_123", stripeClient.getContext());
+
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader);
+
+ assertEquals("original_context_123", stripeClient.getContext());
+ }
+
+ @Test
+ public void testStripeContextRestoredAfterHandlerError()
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ // Test that the original stripe_context is restored even when handler raises an exception
+ StripeEventNotificationHandler.Callback
+ handler =
+ (event, client) -> {
+ assertEquals("event_context_456", client.getContext());
+ throw new RuntimeException("Handler error!");
+ };
+
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+
+ assertEquals("original_context_123", stripeClient.getContext());
+
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+
+ RuntimeException exception =
+ assertThrows(
+ RuntimeException.class,
+ () -> eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader));
+ assertEquals("Handler error!", exception.getMessage());
+
+ assertEquals("original_context_123", stripeClient.getContext());
+ }
+
+ @Test
+ public void testStripeContextSetToNullWhenEventHasNoContext()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that stripe_context is set to null when event context is null
+ AtomicReference receivedContext = new AtomicReference<>();
+
+ StripeEventNotificationHandler.Callback handler =
+ (event, client) -> {
+ receivedContext.set(client.getContext());
+ };
+
+ eventNotificationHandler.onV2CoreAccountCreated(handler);
+
+ assertEquals("original_context_123", stripeClient.getContext());
+
+ String sigHeader = generateSigHeader(v2AccountCreatedPayload);
+ eventNotificationHandler.handle(v2AccountCreatedPayload, sigHeader);
+
+ assertNull(receivedContext.get());
+ assertEquals("original_context_123", stripeClient.getContext());
+ }
+
+ @Test
+ public void testUnknownEventRoutesToOnUnhandled()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that events without SDK types rout handler
+ String sigHeader = generateSigHeader(unknownEventPayload);
+ eventNotificationHandler.handle(unknownEventPayload, sigHeader);
+
+ verify(fallbackCallback, times(1))
+ .process(
+ org.mockito.ArgumentMatchers.argThat(
+ event ->
+ event instanceof UnknownEventNotification
+ && event.getType().equals("llama.created")),
+ org.mockito.ArgumentMatchers.any(StripeClient.class),
+ org.mockito.ArgumentMatchers.argThat(info -> info.isKnownEventType() == false));
+ }
+
+ @Test
+ public void testKnownUnregisteredEventRoutesToOnUnhandled()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that known event types without a registered handler rout
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader);
+
+ verify(fallbackCallback, times(1))
+ .process(
+ org.mockito.ArgumentMatchers.argThat(
+ event ->
+ event instanceof V1BillingMeterErrorReportTriggeredEventNotification
+ && event.getType().equals("v1.billing.meter.error_report_triggered")),
+ org.mockito.ArgumentMatchers.any(StripeClient.class),
+ org.mockito.ArgumentMatchers.argThat(info -> info.isKnownEventType() == true));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testRegisteredEventDoesNotCallOnUnhandled()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that registered events don't tri
+ StripeEventNotificationHandler.Callback
+ handler = mock(StripeEventNotificationHandler.Callback.class);
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader);
+
+ verify(handler, times(1))
+ .process(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+ verify(fallbackCallback, never())
+ .process(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any());
+ }
+
+ @Test
+ public void testHandlerClientRetainsConfiguration()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test that the client passed to handlers retains all configuration except stripe_context
+ String originalContext = "original_context_xyz";
+
+ StripeClient customClient =
+ StripeClient.builder()
+ .setApiKey("sk_test_custom_key")
+ .setStripeContext(originalContext)
+ .build();
+
+ StripeEventNotificationHandler customRouter =
+ new StripeEventNotificationHandler(DUMMY_WEBHOOK_SECRET, customClient, fallbackCallback);
+
+ AtomicReference receivedContext = new AtomicReference<>();
+
+ StripeEventNotificationHandler.Callback
+ handler =
+ (event, client) -> {
+ receivedContext.set(client.getContext());
+ };
+
+ customRouter.onV1BillingMeterErrorReportTriggered(handler);
+
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ customRouter.handle(v1BillingMeterPayload, sigHeader);
+
+ assertEquals("event_context_456", receivedContext.get());
+ assertEquals(originalContext, customClient.getContext());
+ }
+
+ @Test
+ public void testOnUnhandledReceivesCorrectInfoForUnknown()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test receives correct UnhandledNotificationDetails for unknown events
+ String sigHeader = generateSigHeader(unknownEventPayload);
+ eventNotificationHandler.handle(unknownEventPayload, sigHeader);
+
+ verify(fallbackCallback, times(1))
+ .process(
+ org.mockito.ArgumentMatchers.any(EventNotification.class),
+ org.mockito.ArgumentMatchers.any(StripeClient.class),
+ org.mockito.ArgumentMatchers.argThat(info -> info.isKnownEventType() == false));
+ }
+
+ @Test
+ public void testOnUnhandledReceivesCorrectInfoForKnownUnregistered()
+ throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
+ // Test receives correct UnhandledNotificationDetails for known unregistered
+ // events
+ String sigHeader = generateSigHeader(v1BillingMeterPayload);
+ eventNotificationHandler.handle(v1BillingMeterPayload, sigHeader);
+
+ verify(fallbackCallback, times(1))
+ .process(
+ org.mockito.ArgumentMatchers.any(EventNotification.class),
+ org.mockito.ArgumentMatchers.any(StripeClient.class),
+ org.mockito.ArgumentMatchers.argThat(info -> info.isKnownEventType() == true));
+ }
+
+ @Test
+ public void testValidatesWebhookSignature() {
+ // Test that invalid webhook signatures are rejected
+ assertThrows(
+ SignatureVerificationException.class,
+ () -> eventNotificationHandler.handle(v1BillingMeterPayload, "invalid_signature"));
+ }
+
+ @Test
+ public void testRegisteredEventTypesEmpty() {
+ // Test that registered_event_types returns empty list when no handlers are registered
+ List eventTypes = eventNotificationHandler.getRegisteredEventTypes();
+ assertNotNull(eventTypes);
+ assertTrue(eventTypes.isEmpty());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testRegisteredEventTypesSingle() {
+ // Test that registered_event_types returns a single event type
+ StripeEventNotificationHandler.Callback
+ handler = mock(StripeEventNotificationHandler.Callback.class);
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+
+ List eventTypes = eventNotificationHandler.getRegisteredEventTypes();
+ assertEquals(1, eventTypes.size());
+ assertEquals("v1.billing.meter.error_report_triggered", eventTypes.get(0));
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Test
+ public void testRegisteredEventTypesMultipleAlphabetized() {
+ // Test that registered_event_types returns multiple event types in alphabetical order
+ StripeEventNotificationHandler.Callback handler =
+ mock(StripeEventNotificationHandler.Callback.class);
+
+ // Register in non-alphabetical order
+ eventNotificationHandler.onV2CoreAccountUpdated(handler);
+ eventNotificationHandler.onV1BillingMeterErrorReportTriggered(handler);
+ eventNotificationHandler.onV2CoreAccountCreated(handler);
+
+ List expected =
+ Arrays.asList(
+ "v1.billing.meter.error_report_triggered",
+ "v2.core.account.created",
+ "v2.core.account.updated");
+
+ List eventTypes = eventNotificationHandler.getRegisteredEventTypes();
+ assertEquals(expected, eventTypes);
+ }
+}