diff --git a/.vscode/settings.json b/.vscode/settings.json index c7082cd1373..61172939b09 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,9 @@ }, "java.configuration.updateBuildConfiguration": "automatic", // LSP was ooming and it recommended this change - "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable" + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable", + "java.test.config": { + "vmargs": [ "-Dstripe.disallowGlobalResponseGetterFallback=true"] + + } } diff --git a/README.md b/README.md index d6a5993561d..e92687826a0 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,31 @@ If your beta feature requires a `Stripe-Version` header to be sent, set the `Str Stripe.addBetaVersion("feature_beta", "v3"); ``` +### Custom requests + +If you would like to send a request to an undocumented API (for example you are in a private beta), or if you prefer to bypass the method definitions in the library and specify your request details directly, you can use the `rawRequest` method on `StripeClient`. + +```java +// Create a RawRequestOptions object, allowing you to set per-request +// configuration options like additional headers. +Map stripeVersionHeader = new HashMap<>(); +stripeVersionHeader.put("Stripe-Version", "2022-11-15; feature_beta=v3"); +RawRequestOptions options = + RawRequestOptions.builder() + .setAdditionalHeaders(stripeVersionHeader) + .build(); + +// Make the request using the Stripe.rawRequest() method. +StripeClient client = new StripeClient("sk_test_..."); +final StripeResponse response = + client.rawRequest( + ApiResource.RequestMethod.POST, "/v1/beta_endpoint", "param=123", options); + +// (Optional) response.body() is a string. You can call +// Stripe.deserialize() to get a StripeObject. +StripeObject obj = client.deserialize(response.body()); +``` + ## Support New features and bug fixes are released on the latest major version of the Stripe Java client library. If you are on an older major version, we recommend that you upgrade to the latest in order to use the new features and bug fixes including those for security vulnerabilities. Older major versions of the package will continue to be available for use, but will not be receiving any updates. diff --git a/src/main/java/com/stripe/Stripe.java b/src/main/java/com/stripe/Stripe.java index 8ad8117d230..5443281488f 100644 --- a/src/main/java/com/stripe/Stripe.java +++ b/src/main/java/com/stripe/Stripe.java @@ -13,12 +13,12 @@ public abstract class Stripe { public static final String CONNECT_API_BASE = "https://connect.stripe.com"; public static final String LIVE_API_BASE = "https://api.stripe.com"; public static final String UPLOAD_API_BASE = "https://files.stripe.com"; + public static final String METER_EVENTS_API_BASE = "https://meter-events.stripe.com"; public static final String VERSION = "26.12.0"; public static volatile String apiKey; public static volatile String clientId; public static volatile boolean enableTelemetry = true; - public static volatile String partnerId; // Note that URLConnection reserves the value of 0 to mean "infinite // timeout", so we use -1 here to represent an unset value which should @@ -26,14 +26,14 @@ public abstract class Stripe { private static volatile int connectTimeout = -1; private static volatile int readTimeout = -1; - private static volatile int maxNetworkRetries = 0; + private static volatile int maxNetworkRetries = 2; private static volatile String apiBase = LIVE_API_BASE; private static volatile String connectBase = CONNECT_API_BASE; private static volatile String uploadBase = UPLOAD_API_BASE; + private static volatile String meterEventsBase = METER_EVENTS_API_BASE; private static volatile Proxy connectionProxy = null; private static volatile PasswordAuthentication proxyCredential = null; - private static volatile Map appInfo = null; /** @@ -72,6 +72,18 @@ public static String getUploadBase() { return uploadBase; } + /** + * (FOR TESTING ONLY) If you'd like your events requests to hit your own (mocked) server, you can + * set this up here by overriding the base api URL. + */ + public static void overrideMeterEventsBase(final String overriddenMeterEventsBase) { + meterEventsBase = overriddenMeterEventsBase; + } + + public static String getMeterEventsBase() { + return meterEventsBase; + } + /** * Set proxy to tunnel all Stripe connections. * @@ -94,6 +106,7 @@ public static int getConnectTimeout() { if (connectTimeout == -1) { return DEFAULT_CONNECT_TIMEOUT; } + return connectTimeout; } diff --git a/src/main/java/com/stripe/StripeClient.java b/src/main/java/com/stripe/StripeClient.java index d000e326d13..998e3e1d1e3 100644 --- a/src/main/java/com/stripe/StripeClient.java +++ b/src/main/java/com/stripe/StripeClient.java @@ -1,8 +1,11 @@ package com.stripe; import com.stripe.exception.SignatureVerificationException; -import com.stripe.model.Event; +import com.stripe.exception.StripeException; +import com.stripe.model.StripeObject; +import com.stripe.model.ThinEvent; import com.stripe.net.*; +import com.stripe.net.Webhook.Signature; import java.net.PasswordAuthentication; import java.net.Proxy; import lombok.Getter; @@ -38,6 +41,24 @@ protected StripeResponseGetter getResponseGetter() { return responseGetter; } + /** + * 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 + * verification fails for any reason. + * + * @param payload the payload sent by Stripe. + * @param sigHeader the contents of the signature header sent by Stripe. + * @param secret secret used to generate the signature. + * @return the StripeEvent instance + * @throws SignatureVerificationException if the verification fails. + */ + public ThinEvent parseThinEvent(String payload, String sigHeader, String secret) + throws SignatureVerificationException { + Signature.verifyHeader(payload, sigHeader, secret, Webhook.DEFAULT_TOLERANCE); + + return ApiResource.GSON.fromJson(payload, ThinEvent.class); + } + /** * Returns an Event instance using the provided JSON payload. Throws a JsonSyntaxException if the * payload is not valid JSON, and a SignatureVerificationException if the signature verification @@ -49,9 +70,9 @@ protected StripeResponseGetter getResponseGetter() { * @return the Event instance * @throws SignatureVerificationException if the verification fails. */ - public Event constructEvent(String payload, String sigHeader, String secret) + public com.stripe.model.Event parseSnapshotEvent(String payload, String sigHeader, String secret) throws SignatureVerificationException { - Event event = Webhook.constructEvent(payload, sigHeader, secret); + com.stripe.model.Event event = Webhook.constructEvent(payload, sigHeader, secret); event.setResponseGetter(this.getResponseGetter()); return event; } @@ -69,9 +90,10 @@ public Event constructEvent(String payload, String sigHeader, String secret) * @return the Event instance * @throws SignatureVerificationException if the verification fails. */ - public Event constructEvent(String payload, String sigHeader, String secret, long tolerance) + public com.stripe.model.Event parseSnapshotEvent( + String payload, String sigHeader, String secret, long tolerance) throws SignatureVerificationException { - Event event = Webhook.constructEvent(payload, sigHeader, secret, tolerance); + com.stripe.model.Event event = Webhook.constructEvent(payload, sigHeader, secret, tolerance); event.setResponseGetter(this.getResponseGetter()); return event; } @@ -345,6 +367,10 @@ public com.stripe.service.TreasuryService treasury() { return new com.stripe.service.TreasuryService(this.getResponseGetter()); } + public com.stripe.service.V2Services v2() { + return new com.stripe.service.V2Services(this.getResponseGetter()); + } + public com.stripe.service.WebhookEndpointService webhookEndpoints() { return new com.stripe.service.WebhookEndpointService(this.getResponseGetter()); } @@ -354,7 +380,7 @@ static class ClientStripeResponseGetterOptions extends StripeResponseGetterOptio // When adding setting here keep them in sync with settings in RequestOptions and // in the RequestOptions.merge method @Getter(onMethod_ = {@Override}) - private final String apiKey; + private final Authenticator authenticator; @Getter(onMethod_ = {@Override}) private final String clientId; @@ -383,8 +409,14 @@ static class ClientStripeResponseGetterOptions extends StripeResponseGetterOptio @Getter(onMethod_ = {@Override}) private final String connectBase; + @Getter(onMethod_ = {@Override}) + private final String meterEventsBase; + + @Getter(onMethod_ = {@Override}) + private final String stripeContext; + ClientStripeResponseGetterOptions( - String apiKey, + Authenticator authenticator, String clientId, int connectTimeout, int readTimeout, @@ -393,8 +425,10 @@ static class ClientStripeResponseGetterOptions extends StripeResponseGetterOptio PasswordAuthentication proxyCredential, String apiBase, String filesBase, - String connectBase) { - this.apiKey = apiKey; + String connectBase, + String meterEventsBase, + String stripeContext) { + this.authenticator = authenticator; this.clientId = clientId; this.connectTimeout = connectTimeout; this.readTimeout = readTimeout; @@ -404,6 +438,8 @@ static class ClientStripeResponseGetterOptions extends StripeResponseGetterOptio this.apiBase = apiBase; this.filesBase = filesBase; this.connectBase = connectBase; + this.meterEventsBase = meterEventsBase; + this.stripeContext = stripeContext; } } @@ -416,7 +452,7 @@ public static StripeClientBuilder builder() { } public static final class StripeClientBuilder { - private String apiKey; + private Authenticator authenticator; private String clientId; private int connectTimeout = Stripe.DEFAULT_CONNECT_TIMEOUT; private int readTimeout = Stripe.DEFAULT_READ_TIMEOUT; @@ -426,6 +462,8 @@ public static final class StripeClientBuilder { private String apiBase = Stripe.LIVE_API_BASE; private String filesBase = Stripe.UPLOAD_API_BASE; private String connectBase = Stripe.CONNECT_API_BASE; + private String meterEventsBase = Stripe.METER_EVENTS_API_BASE; + private String stripeContext; /** * Constructs a request options builder with the global parameters (API key and client ID) as @@ -433,17 +471,34 @@ public static final class StripeClientBuilder { */ public StripeClientBuilder() {} + public Authenticator getAuthenticator() { + return this.authenticator; + } + + public StripeClientBuilder setAuthenticator(Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + public String getApiKey() { - return this.apiKey; + if (authenticator instanceof BearerTokenAuthenticator) { + return ((BearerTokenAuthenticator) authenticator).getApiKey(); + } + + return null; } - /** - * Set API key to use for authenticating requests. - * - * @param apiKey API key - */ public StripeClientBuilder setApiKey(String apiKey) { - this.apiKey = apiKey; + if (apiKey == null) { + this.authenticator = null; + } else { + this.authenticator = new BearerTokenAuthenticator(apiKey); + } + return this; + } + + public StripeClientBuilder clearApiKey() { + this.authenticator = null; return this; } @@ -575,22 +630,42 @@ public StripeClientBuilder setConnectBase(String address) { return this; } - public String getConnectBase() { - return this.connectBase; + /** + * Set the base URL for the Stripe Meter Events API. By default this is + * "https://events.stripe.com". + * + *

This only affects requests made with a {@link com.stripe.net.BaseAddress} of EVENTMES. + */ + public StripeClientBuilder setMeterEventsBase(String address) { + this.meterEventsBase = address; + return this; + } + + public String getMeterEventsBase() { + return this.meterEventsBase; + } + + public StripeClientBuilder setStripeContext(String context) { + this.stripeContext = context; + return this; + } + + public String getStripeContext() { + return this.stripeContext; } - /** Constructs a {@link StripeClient} with the specified configuration. */ + /** Constructs a {@link StripeResponseGetterOptions} with the specified values. */ public StripeClient build() { return new StripeClient(new LiveStripeResponseGetter(buildOptions(), null)); } StripeResponseGetterOptions buildOptions() { - if (this.apiKey == null) { + if (this.authenticator == null) { throw new IllegalArgumentException( - "No API key provided. Use setApiKey to set the Stripe API key"); + "No authentication settings provided. Use setApiKey to set the Stripe API key"); } return new ClientStripeResponseGetterOptions( - this.apiKey, + this.authenticator, this.clientId, connectTimeout, readTimeout, @@ -599,7 +674,60 @@ StripeResponseGetterOptions buildOptions() { proxyCredential, apiBase, filesBase, - connectBase); + connectBase, + meterEventsBase, + this.stripeContext); } } + + /** + * Send raw request to Stripe API. This is the lowest level method for interacting with the Stripe + * API. This method is useful for interacting with endpoints that are not covered yet in + * stripe-java. + * + * @param method the HTTP method + * @param relativeUrl the relative URL of the request, e.g. "/v1/charges" + * @param content the body of the request as a string + * @return the JSON response as a string + */ + public StripeResponse rawRequest( + final ApiResource.RequestMethod method, final String relativeUrl, final String content) + throws StripeException { + return rawRequest(method, relativeUrl, content, null); + } + + /** + * Send raw request to Stripe API. This is the lowest level method for interacting with the Stripe + * API. This method is useful for interacting with endpoints that are not covered yet in + * stripe-java. + * + * @param method the HTTP method + * @param relativeUrl the relative URL of the request, e.g. "/v1/charges" + * @param content the body of the request as a string + * @param options the special modifiers of the request + * @return the JSON response as a string + */ + public StripeResponse rawRequest( + final ApiResource.RequestMethod method, + final String relativeUrl, + final String content, + RawRequestOptions options) + throws StripeException { + if (options == null) { + options = RawRequestOptions.builder().build(); + } + if (method != ApiResource.RequestMethod.POST && content != null && !content.equals("")) { + throw new IllegalArgumentException( + "content is not allowed for non-POST requests. Please pass null and add request parameters to the query string of the URL."); + } + RawApiRequest req = new RawApiRequest(BaseAddress.API, method, relativeUrl, content, options); + req = req.addUsage("stripe_client"); + req = req.addUsage("raw_request"); + return this.getResponseGetter().rawRequest(req); + } + + /** Deserializes StripeResponse returned by rawRequest into a similar class. */ + public StripeObject deserialize(String rawJson) throws StripeException { + return StripeObject.deserializeStripeObject(rawJson, this.getResponseGetter()); + } } diff --git a/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEvent.java b/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEvent.java new file mode 100644 index 00000000000..0d70dcb7f5a --- /dev/null +++ b/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEvent.java @@ -0,0 +1,89 @@ +// File generated from our OpenAPI spec +package com.stripe.events; + +import com.google.gson.annotations.SerializedName; +import com.stripe.exception.StripeException; +import com.stripe.model.billing.Meter; +import com.stripe.model.v2.Event; +import java.time.Instant; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +public final class V1BillingMeterErrorReportTriggeredEvent extends Event { + /** Data for the v1.billing.meter.error_report_triggered event. */ + @SerializedName("data") + V1BillingMeterErrorReportTriggeredEvent.EventData data; + + @Getter + @Setter + public static final class EventData { + /** Extra field included in the event's {@code data} when fetched from /v2/events. */ + @SerializedName("developer_message_summary") + String developerMessageSummary; + /** This contains information about why meter error happens. */ + @SerializedName("reason") + Reason reason; + /** The end of the window that is encapsulated by this summary. */ + @SerializedName("validation_end") + Instant validationEnd; + /** The start of the window that is encapsulated by this summary. */ + @SerializedName("validation_start") + Instant validationStart; + + public static final class Reason { + /** The total error count within this window. */ + @SerializedName("error_count") + Integer errorCount; + /** The error details. */ + @SerializedName("error_types") + List errorTypes; + + public static final class ErrorType { + /** + * Open Enum. + * + *

One of {@code archived_meter}, {@code meter_event_customer_not_found}, {@code + * meter_event_dimension_count_too_high}, {@code meter_event_invalid_value}, {@code + * meter_event_no_customer_defined}, {@code missing_dimension_payload_keys}, {@code + * no_meter}, {@code timestamp_in_future}, or {@code timestamp_too_far_in_past}. + */ + @SerializedName("code") + String code; + /** The number of errors of this type. */ + @SerializedName("error_count") + Integer errorCount; + /** A list of sample errors of this type. */ + @SerializedName("sample_errors") + List + sampleErrors; + + public static final class SampleError { + /** The error message. */ + @SerializedName("error_message") + String errorMessage; + /** The request causes the error. */ + @SerializedName("request") + Request request; + + public static final class Request { + /** The request idempotency key. */ + @SerializedName("identifier") + String identifier; + } + } + } + } + } + + @SerializedName("related_object") + + /** Object containing the reference to API resource relevant to the event. */ + RelatedObject relatedObject; + + /** Retrieves the related object from the API. Make an API request on every call. */ + public Meter fetchRelatedObject() throws StripeException { + return (Meter) super.fetchRelatedObject(this.relatedObject); + } +} diff --git a/src/main/java/com/stripe/events/V1BillingMeterNoMeterFoundEvent.java b/src/main/java/com/stripe/events/V1BillingMeterNoMeterFoundEvent.java new file mode 100644 index 00000000000..e896ebe7687 --- /dev/null +++ b/src/main/java/com/stripe/events/V1BillingMeterNoMeterFoundEvent.java @@ -0,0 +1,76 @@ +// File generated from our OpenAPI spec +package com.stripe.events; + +import com.google.gson.annotations.SerializedName; +import com.stripe.model.v2.Event; +import java.time.Instant; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +public final class V1BillingMeterNoMeterFoundEvent extends Event { + /** Data for the v1.billing.meter.no_meter_found event. */ + @SerializedName("data") + V1BillingMeterNoMeterFoundEvent.EventData data; + + @Getter + @Setter + public static final class EventData { + /** Extra field included in the event's {@code data} when fetched from /v2/events. */ + @SerializedName("developer_message_summary") + String developerMessageSummary; + /** This contains information about why meter error happens. */ + @SerializedName("reason") + Reason reason; + /** The end of the window that is encapsulated by this summary. */ + @SerializedName("validation_end") + Instant validationEnd; + /** The start of the window that is encapsulated by this summary. */ + @SerializedName("validation_start") + Instant validationStart; + + public static final class Reason { + /** The total error count within this window. */ + @SerializedName("error_count") + Integer errorCount; + /** The error details. */ + @SerializedName("error_types") + List errorTypes; + + public static final class ErrorType { + /** + * Open Enum. + * + *

One of {@code archived_meter}, {@code meter_event_customer_not_found}, {@code + * meter_event_dimension_count_too_high}, {@code meter_event_invalid_value}, {@code + * meter_event_no_customer_defined}, {@code missing_dimension_payload_keys}, {@code + * no_meter}, {@code timestamp_in_future}, or {@code timestamp_too_far_in_past}. + */ + @SerializedName("code") + String code; + /** The number of errors of this type. */ + @SerializedName("error_count") + Integer errorCount; + /** A list of sample errors of this type. */ + @SerializedName("sample_errors") + List sampleErrors; + + public static final class SampleError { + /** The error message. */ + @SerializedName("error_message") + String errorMessage; + /** The request causes the error. */ + @SerializedName("request") + Request request; + + public static final class Request { + /** The request idempotency key. */ + @SerializedName("identifier") + String identifier; + } + } + } + } + } +} diff --git a/src/main/java/com/stripe/examples/MeterEventManager.java b/src/main/java/com/stripe/examples/MeterEventManager.java new file mode 100644 index 00000000000..9f9e142bf33 --- /dev/null +++ b/src/main/java/com/stripe/examples/MeterEventManager.java @@ -0,0 +1,61 @@ +package com.stripe.examples; + +import com.stripe.StripeClient; +import com.stripe.model.v2.billing.MeterEventSession; +import com.stripe.param.v2.billing.MeterEventStreamCreateParams; +import java.time.Instant; + +public class MeterEventManager { + + private String apiKey; + private MeterEventSession meterEventSession; + + public MeterEventManager(String apiKey) { + this.apiKey = apiKey; + } + + @SuppressWarnings("CatchAndPrintStackTrace") + private void refreshMeterEventSession() { + if (meterEventSession == null || meterEventSession.getExpiresAt().isBefore(Instant.now())) { + // Create a new meter event session in case the existing session expired + try { + StripeClient client = new StripeClient(apiKey); + meterEventSession = client.v2().billing().meterEventSession().create(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @SuppressWarnings("CatchAndPrintStackTrace") + public void sendMeterEvent(String eventName, String stripeCustomerId, String value) { + // Refresh the meter event session, if necessary + refreshMeterEventSession(); + + // Create a meter event + + MeterEventStreamCreateParams.Event eventParams = + MeterEventStreamCreateParams.Event.builder() + .setEventName(eventName) + .putPayload("stripe_customer_id", stripeCustomerId) + .putPayload("value", value) + .build(); + MeterEventStreamCreateParams params = + MeterEventStreamCreateParams.builder().addEvent(eventParams).build(); + + try { + StripeClient client = new StripeClient(meterEventSession.getAuthenticationToken()); + client.v2().billing().meterEventStream().create(params); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + String apiKey = "{{API_KEY}}"; + String customerId = "{{CUSTOMER_ID}}"; // Replace with actual customer ID + + MeterEventManager manager = new MeterEventManager(apiKey); + manager.sendMeterEvent("alpaca_ai_tokens", customerId, "28"); + } +} diff --git a/src/main/java/com/stripe/examples/NewExample.java b/src/main/java/com/stripe/examples/NewExample.java new file mode 100644 index 00000000000..908307b1601 --- /dev/null +++ b/src/main/java/com/stripe/examples/NewExample.java @@ -0,0 +1,36 @@ +package com.stripe.examples; + +/** + * To create a new example, clone this file and implement your example. + * + *

To run from VS Code: 1. make sure the recommended extensions are installed 2. right click on + * your new file in the Explorer 3. click Run Java or Debug Java 4. witness greatness. + */ +public class NewExample { + + @SuppressWarnings("unused") + private String apiKey; + + public NewExample(String apiKey) { + this.apiKey = apiKey; + } + + @SuppressWarnings("CatchAndPrintStackTrace") + public void doSomethingGreat() { + + try { + System.out.println("Hello World"); + // StripeClient client = new StripeClient(this.apiKey); + // client.v2().... + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + String apiKey = "{{API_KEY}}"; + + NewExample example = new NewExample(apiKey); + example.doSomethingGreat(); + } +} diff --git a/src/main/java/com/stripe/examples/StripeWebhookHandler.java b/src/main/java/com/stripe/examples/StripeWebhookHandler.java new file mode 100644 index 00000000000..bf91afd1e9e --- /dev/null +++ b/src/main/java/com/stripe/examples/StripeWebhookHandler.java @@ -0,0 +1,52 @@ +package com.stripe.examples; + +public class StripeWebhookHandler { + // private static final String API_KEY = System.getenv("STRIPE_API_KEY"); + // private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET"); + + // private static final StripeClient client = new StripeClient(API_KEY); + + // public static void main(String[] args) throws IOException { + + // HttpServer server = HttpServer.create(new InetSocketAddress(4242), 0); + // server.createContext("/webhook", new WebhookHandler()); + // server.setExecutor(null); + // server.start(); + // } + + // static class WebhookHandler implements HttpHandler { + // @Override + // public void handle(HttpExchange exchange) throws IOException { + // if ("POST".equals(exchange.getRequestMethod())) { + // InputStream requestBody = exchange.getRequestBody(); + // String webhookBody = new String(requestBody.readAllBytes(), StandardCharsets.UTF_8); + // String sigHeader = exchange.getRequestHeaders().getFirst("Stripe-Signature"); + + // try { + // ThinEvent thinEvent = client.parseThinEvent(webhookBody, sigHeader, WEBHOOK_SECRET); + + // // Fetch the event data to understand the failure + // Event baseEvent = client.v2().core().events().retrieve(thinEvent.getId()); + // if (baseEvent instanceof V1BillingMeterErrorReportTriggeredEvent) { + // V1BillingMeterErrorReportTriggeredEvent event = + // (V1BillingMeterErrorReportTriggeredEvent) baseEvent; + // Meter meter = event.fetchRelatedObject(); + + // String meterId = meter.getId(); + // System.out.println(meterId); + + // // Record the failures and alert your team + // // Add your logic here + // } + + // exchange.sendResponseHeaders(200, -1); + // } catch (StripeException e) { + // exchange.sendResponseHeaders(400, -1); + // } + // } else { + // exchange.sendResponseHeaders(405, -1); + // } + // exchange.close(); + // } + // } +} diff --git a/src/main/java/com/stripe/exception/AuthenticationException.java b/src/main/java/com/stripe/exception/AuthenticationException.java index c18a8d725bb..9a16aa2d8bc 100644 --- a/src/main/java/com/stripe/exception/AuthenticationException.java +++ b/src/main/java/com/stripe/exception/AuthenticationException.java @@ -7,4 +7,9 @@ public AuthenticationException( String message, String requestId, String code, Integer statusCode) { super(message, requestId, code, statusCode); } + + public AuthenticationException( + String message, String requestId, String code, Integer statusCode, Throwable e) { + super(message, requestId, code, statusCode, e); + } } diff --git a/src/main/java/com/stripe/exception/IdempotencyException.java b/src/main/java/com/stripe/exception/IdempotencyException.java deleted file mode 100644 index a89d9a2bef6..00000000000 --- a/src/main/java/com/stripe/exception/IdempotencyException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.stripe.exception; - -public class IdempotencyException extends StripeException { - private static final long serialVersionUID = 2L; - - public IdempotencyException(String message, String requestId, String code, Integer statusCode) { - super(message, requestId, code, statusCode); - } -} diff --git a/src/main/java/com/stripe/exception/StripeException.java b/src/main/java/com/stripe/exception/StripeException.java index 8aeda821619..09479171da9 100644 --- a/src/main/java/com/stripe/exception/StripeException.java +++ b/src/main/java/com/stripe/exception/StripeException.java @@ -1,8 +1,9 @@ package com.stripe.exception; +import com.google.gson.JsonObject; import com.stripe.model.StripeError; +import com.stripe.net.StripeResponseGetter; import lombok.Getter; -import lombok.Setter; @Getter public abstract class StripeException extends Exception { @@ -11,7 +12,11 @@ public abstract class StripeException extends Exception { /** The error resource returned by Stripe's API that caused the exception. */ // transient so the exception can be serialized, as StripeObject does not // implement Serializable - @Setter transient StripeError stripeError; + transient StripeError stripeError; + + public void setStripeError(StripeError err) { + stripeError = err; + } /** * Returns the error code of the response that triggered this exception. For {@link ApiException} @@ -63,6 +68,9 @@ public String getMessage() { if (requestId != null) { additionalInfo += "; request-id: " + requestId; } + if (this.getUserMessage() != null) { + additionalInfo += "; user-message: " + this.getUserMessage(); + } return super.getMessage() + additionalInfo; } @@ -72,6 +80,25 @@ public String getMessage() { * @return a string representation of the user facing exception. */ public String getUserMessage() { - return super.getMessage(); + if (this.getStripeError() != null) { + return this.getStripeError().getUserMessage(); + } + return null; + } + + public static StripeException parseV2Exception( + String type, + JsonObject body, + int statusCode, + String requestId, + StripeResponseGetter responseGetter) { + switch (type) { + // The beginning of the section generated from our OpenAPI spec + case "temporary_session_expired": + return com.stripe.exception.TemporarySessionExpiredException.parse( + body, statusCode, requestId, responseGetter); + // The end of the section generated from our OpenAPI spec + } + return null; } } diff --git a/src/main/java/com/stripe/exception/TemporarySessionExpiredException.java b/src/main/java/com/stripe/exception/TemporarySessionExpiredException.java new file mode 100644 index 00000000000..376c73a885b --- /dev/null +++ b/src/main/java/com/stripe/exception/TemporarySessionExpiredException.java @@ -0,0 +1,34 @@ +// File generated from our OpenAPI spec +package com.stripe.exception; + +import com.google.gson.JsonObject; +import com.stripe.model.StripeError; +import com.stripe.model.StripeObject; +import com.stripe.net.StripeResponseGetter; + +/** The temporary session token has expired. */ +public final class TemporarySessionExpiredException extends ApiException { + private static final long serialVersionUID = 2L; + + private TemporarySessionExpiredException( + String message, String requestId, String code, Integer statusCode, Throwable e) { + super(message, requestId, code, statusCode, e); + } + + static TemporarySessionExpiredException parse( + JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) { + TemporarySessionExpiredException.TemporarySessionExpiredError error = + (TemporarySessionExpiredException.TemporarySessionExpiredError) + StripeObject.deserializeStripeObject( + body, + TemporarySessionExpiredException.TemporarySessionExpiredError.class, + responseGetter); + TemporarySessionExpiredException exception = + new TemporarySessionExpiredException( + error.getMessage(), requestId, error.getCode(), statusCode, null); + exception.setStripeError(error); + return exception; + } + + public static class TemporarySessionExpiredError extends StripeError {} +} diff --git a/src/main/java/com/stripe/model/InstantDeserializer.java b/src/main/java/com/stripe/model/InstantDeserializer.java new file mode 100644 index 00000000000..64447cc1f4a --- /dev/null +++ b/src/main/java/com/stripe/model/InstantDeserializer.java @@ -0,0 +1,32 @@ +package com.stripe.model; + +import com.google.gson.*; +import java.lang.reflect.Type; +import java.time.Instant; + +public class InstantDeserializer implements JsonDeserializer, JsonSerializer { + /** Deserializes an timestamp JSON payload into an {@link java.time.Instant} object. */ + @Override + public Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + if (json.isJsonNull()) { + return null; + } + + if (json.isJsonPrimitive()) { + JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive(); + if (jsonPrimitive.isString()) { + return Instant.parse(jsonPrimitive.getAsString()); + } + + throw new JsonParseException("Instant is a non-string primitive type."); + } + + throw new JsonParseException("Instant is a non-primitive type."); + } + + @Override + public JsonElement serialize(Instant src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } +} diff --git a/src/main/java/com/stripe/model/InstantSerializer.java b/src/main/java/com/stripe/model/InstantSerializer.java new file mode 100644 index 00000000000..580fb234961 --- /dev/null +++ b/src/main/java/com/stripe/model/InstantSerializer.java @@ -0,0 +1,19 @@ +package com.stripe.model; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import java.time.Instant; + +public class InstantSerializer implements JsonSerializer { + /** Serializes an Instant into a JSON string in ISO 8601 format. */ + @Override + public JsonElement serialize(Instant src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return new JsonPrimitive(src.toString()); + } + return null; + } +} diff --git a/src/main/java/com/stripe/model/StripeCollection.java b/src/main/java/com/stripe/model/StripeCollection.java index 60b568a64d4..59d0586d25c 100644 --- a/src/main/java/com/stripe/model/StripeCollection.java +++ b/src/main/java/com/stripe/model/StripeCollection.java @@ -1,7 +1,6 @@ package com.stripe.model; -import com.stripe.net.RequestOptions; -import com.stripe.net.StripeResponseGetter; +import com.stripe.net.*; import java.lang.reflect.Type; import java.util.List; import java.util.Map; @@ -40,6 +39,7 @@ public class StripeCollection extends StripeObject implements StripeCollectionInterface, StripeActiveObject { private transient StripeResponseGetter responseGetter; + String object; @Getter(onMethod_ = {@Override}) @@ -69,7 +69,6 @@ public Iterable autoPagingIterable() { public Iterable autoPagingIterable(Map params) { this.responseGetter.validateRequestOptions(this.requestOptions); - this.setRequestParams(params); return new PagingIterable<>(this, responseGetter, pageTypeToken); } @@ -92,6 +91,7 @@ public Iterable autoPagingIterable(Map params, RequestOptions @Override public void setResponseGetter(StripeResponseGetter responseGetter) { this.responseGetter = responseGetter; + if (this.data != null) { for (T item : data) { trySetResponseGetter(item, responseGetter); diff --git a/src/main/java/com/stripe/model/StripeError.java b/src/main/java/com/stripe/model/StripeError.java index ac6c8425fc4..2382a2293c3 100644 --- a/src/main/java/com/stripe/model/StripeError.java +++ b/src/main/java/com/stripe/model/StripeError.java @@ -213,4 +213,8 @@ public class StripeError extends StripeObject { */ @SerializedName("type") String type; + + /** The user message associated with the error. */ + @SerializedName("user_message") + String userMessage; } diff --git a/src/main/java/com/stripe/model/StripeObject.java b/src/main/java/com/stripe/model/StripeObject.java index 825370c6045..08586e7b46b 100644 --- a/src/main/java/com/stripe/model/StripeObject.java +++ b/src/main/java/com/stripe/model/StripeObject.java @@ -9,6 +9,7 @@ import com.stripe.net.StripeResponseGetter; import java.lang.reflect.Field; import java.lang.reflect.Type; +import java.time.Instant; public abstract class StripeObject implements StripeObjectInterface { public static final Gson PRETTY_PRINT_GSON = @@ -17,6 +18,7 @@ public abstract class StripeObject implements StripeObjectInterface { .serializeNulls() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapter(ExpandableField.class, new ExpandableFieldSerializer()) + .registerTypeAdapter(Instant.class, new InstantSerializer()) .create(); private transient StripeResponse lastResponse; @@ -101,11 +103,17 @@ static StripeObject deserializeStripeObject( String type = eventDataObjectJson.getAsJsonObject().get("object").getAsString(); Class cl = EventDataClassLookup.classLookup.get(type); StripeObject object = - ApiResource.deserializeStripeObject( + StripeObject.deserializeStripeObject( eventDataObjectJson, cl != null ? cl : StripeRawJsonObject.class, responseGetter); return object; } + public static StripeObject deserializeStripeObject( + String payload, StripeResponseGetter responseGetter) { + JsonObject jsonObject = ApiResource.GSON.fromJson(payload, JsonObject.class).getAsJsonObject(); + return deserializeStripeObject(jsonObject, responseGetter); + } + public static StripeObject deserializeStripeObject( JsonObject payload, Type type, StripeResponseGetter responseGetter) { StripeObject object = ApiResource.INTERNAL_GSON.fromJson(payload, type); diff --git a/src/main/java/com/stripe/model/StripeSearchResult.java b/src/main/java/com/stripe/model/StripeSearchResult.java index 94192de7d9b..d843e6b5b7d 100644 --- a/src/main/java/com/stripe/model/StripeSearchResult.java +++ b/src/main/java/com/stripe/model/StripeSearchResult.java @@ -15,7 +15,9 @@ @EqualsAndHashCode(callSuper = false) public class StripeSearchResult extends StripeObject implements StripeSearchResultInterface, StripeActiveObject { + private transient StripeResponseGetter responseGetter; + String object; @Getter(onMethod_ = {@Override}) diff --git a/src/main/java/com/stripe/model/TODO.java b/src/main/java/com/stripe/model/TODO.java new file mode 100644 index 00000000000..8c4437f1618 --- /dev/null +++ b/src/main/java/com/stripe/model/TODO.java @@ -0,0 +1,7 @@ +package com.stripe.model; + +/** + * Represents a type that cannot properly be generated, due to a problem with the generator or a + * non-standard API definition. + */ +public class TODO extends StripeObject {} diff --git a/src/main/java/com/stripe/model/ThinEvent.java b/src/main/java/com/stripe/model/ThinEvent.java new file mode 100644 index 00000000000..b3015f89477 --- /dev/null +++ b/src/main/java/com/stripe/model/ThinEvent.java @@ -0,0 +1,25 @@ +package com.stripe.model; + +import com.google.gson.annotations.SerializedName; +import java.time.Instant; +import lombok.Getter; + +@Getter +public class ThinEvent { + @SerializedName("id") + public String id; + + @SerializedName("type") + public String type; + + @SerializedName("created") + public Instant created; + + // this is optional, and may be null + @SerializedName("context") + public String context; + + // this is optional, and may be null + @SerializedName("related_object") + public ThinEventRelatedObject relatedObject; +} diff --git a/src/main/java/com/stripe/model/ThinEventRelatedObject.java b/src/main/java/com/stripe/model/ThinEventRelatedObject.java new file mode 100644 index 00000000000..48ea526eca3 --- /dev/null +++ b/src/main/java/com/stripe/model/ThinEventRelatedObject.java @@ -0,0 +1,16 @@ +package com.stripe.model; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; + +@Getter +public class ThinEventRelatedObject { + @SerializedName("id") + public String id; + + @SerializedName("type") + public String type; + + @SerializedName("url") + public String url; +} diff --git a/src/main/java/com/stripe/model/v2/Event.java b/src/main/java/com/stripe/model/v2/Event.java new file mode 100644 index 00000000000..e74afac8178 --- /dev/null +++ b/src/main/java/com/stripe/model/v2/Event.java @@ -0,0 +1,154 @@ +// File generated from our OpenAPI spec +package com.stripe.model.v2; + +import com.google.gson.annotations.SerializedName; +import com.stripe.exception.StripeException; +import com.stripe.model.HasId; +import com.stripe.model.StripeActiveObject; +import com.stripe.model.StripeObject; +import com.stripe.model.StripeRawJsonObject; +import com.stripe.net.ApiRequest; +import com.stripe.net.ApiResource; +import com.stripe.net.BaseAddress; +import com.stripe.net.RequestOptions; +import com.stripe.net.StripeResponseGetter; +import java.time.Instant; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +public class Event extends StripeObject implements HasId, StripeActiveObject { + /** Authentication context needed to fetch the event or related object. */ + @SerializedName("context") + String context; + + /** Time at which the object was created. */ + @SerializedName("created") + Instant created; + + /** Unique identifier for the event. */ + @Getter(onMethod_ = {@Override}) + @SerializedName("id") + String id; + + /** + * Has the value {@code true} if the object exists in live mode or the value {@code false} if the + * object exists in test mode. + */ + @SerializedName("livemode") + Boolean livemode; + + /** + * String representing the object's type. Objects of the same type share the same value of the + * object field. + * + *

Equal to {@code v2.core.event}. + */ + @SerializedName("object") + String object; + + /** Reason for the event. */ + @SerializedName("reason") + Reason reason; + + /** The type of the event. */ + @SerializedName("type") + String type; + + StripeResponseGetter responseGetter; + + @Override + public void setResponseGetter(StripeResponseGetter responseGetter) { + this.responseGetter = responseGetter; + } + + /** Retrieves the object associated with the event. */ + protected StripeObject fetchRelatedObject(RelatedObject relatedObject) throws StripeException { + if (relatedObject == null) { + return null; + } + if (relatedObject.getUrl() == null) { + return null; + } + + Class objectClass = + EventDataClassLookup.classLookup.get(relatedObject.getType()); + if (objectClass == null) { + objectClass = StripeRawJsonObject.class; + } + + RequestOptions opts = null; + + if (context != null) { + opts = new RequestOptions.RequestOptionsBuilder().setStripeAccount(context).build(); + } + + return this.responseGetter.request( + new ApiRequest( + BaseAddress.API, ApiResource.RequestMethod.GET, relatedObject.getUrl(), null, opts), + objectClass); + } + + /** + * Returns an StripeEvent instance using the provided JSON payload. Throws a JsonSyntaxException + * if the payload is not valid JSON. + * + * @param payload the payload sent by Stripe. + * @return the StripeEvent instance + */ + public static Event parse(String payload) { + return ApiResource.GSON.fromJson(payload, Event.class); + } + + @Getter + @Setter + @EqualsAndHashCode(callSuper = false) + public static class RelatedObject extends StripeObject implements HasId { + /** Unique identifier for the object relevant to the event. */ + @Getter(onMethod_ = {@Override}) + @SerializedName("id") + String id; + + /** Type of the object relevant to the event. */ + @SerializedName("type") + String type; + + /** Type of the object relevant to the event. */ + @SerializedName("url") + String url; + } + + @Getter + @Setter + @EqualsAndHashCode(callSuper = false) + public static class Reason extends StripeObject { + /** Information on the API request that instigated the event. */ + @SerializedName("request") + Request request; + + /** + * Open Enum. Event reason type. + * + *

Equal to {@code request}. + */ + @SerializedName("type") + String type; + + @Getter + @Setter + @EqualsAndHashCode(callSuper = false) + public static class Request extends StripeObject implements HasId { + /** ID of the API request that caused the event. */ + @Getter(onMethod_ = {@Override}) + @SerializedName("id") + String id; + + /** The idempotency key transmitted during the request. */ + @SerializedName("idempotency_key") + String idempotencyKey; + } + } +} diff --git a/src/main/java/com/stripe/model/v2/EventDataClassLookup.java b/src/main/java/com/stripe/model/v2/EventDataClassLookup.java new file mode 100644 index 00000000000..17cf6e151cf --- /dev/null +++ b/src/main/java/com/stripe/model/v2/EventDataClassLookup.java @@ -0,0 +1,33 @@ +// File generated from our OpenAPI spec +package com.stripe.model.v2; + +import com.stripe.model.StripeObject; +import java.util.HashMap; +import java.util.Map; + +/** + * Event data class look up used in event deserialization. The key to look up is `object` string of + * the model. + */ +final class EventDataClassLookup { + public static final Map> classLookup = new HashMap<>(); + public static final Map> eventClassLookup = new HashMap<>(); + + static { + classLookup.put("billing.meter", com.stripe.model.billing.Meter.class); + + classLookup.put("billing.meter_event", com.stripe.model.v2.billing.MeterEvent.class); + classLookup.put( + "billing.meter_event_adjustment", com.stripe.model.v2.billing.MeterEventAdjustment.class); + classLookup.put( + "billing.meter_event_session", com.stripe.model.v2.billing.MeterEventSession.class); + + classLookup.put("v2.core.event", com.stripe.model.v2.Event.class); + + eventClassLookup.put( + "v1.billing.meter.error_report_triggered", + com.stripe.events.V1BillingMeterErrorReportTriggeredEvent.class); + eventClassLookup.put( + "v1.billing.meter.no_meter_found", com.stripe.events.V1BillingMeterNoMeterFoundEvent.class); + } +} diff --git a/src/main/java/com/stripe/model/v2/EventTypeAdapterFactory.java b/src/main/java/com/stripe/model/v2/EventTypeAdapterFactory.java new file mode 100644 index 00000000000..d8d56fc567a --- /dev/null +++ b/src/main/java/com/stripe/model/v2/EventTypeAdapterFactory.java @@ -0,0 +1,59 @@ +package com.stripe.model.v2; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class EventTypeAdapterFactory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type == null) { + return null; + } + + if (!Event.class.equals(type.getRawType())) { + return null; + } + + final TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); + final TypeAdapter fallbackAdapter = gson.getDelegateAdapter(this, type); + final Map> eventAdapters = new LinkedHashMap<>(); + + for (Map.Entry> entry : + EventDataClassLookup.eventClassLookup.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + eventAdapters.put(entry.getKey(), delegate); + } + + return new TypeAdapter() { + @Override + @SuppressWarnings("unchecked") + public R read(JsonReader in) throws IOException { + JsonElement jsonElement = elementAdapter.read(in); + JsonElement typeElement = jsonElement.getAsJsonObject().get("type"); + + TypeAdapter selectedAdapter = null; + + if (typeElement != null && !typeElement.isJsonNull()) { + String eventType = typeElement.getAsString(); + selectedAdapter = (TypeAdapter) eventAdapters.get(eventType); + } + + if (selectedAdapter == null) { + selectedAdapter = fallbackAdapter; + } + + return selectedAdapter.fromJsonTree(jsonElement); + } + + @Override + public void write(JsonWriter out, R value) throws IOException { + throw new UnsupportedOperationException(); + } + }.nullSafe(); + } +} diff --git a/src/main/java/com/stripe/model/v2/StripeCollection.java b/src/main/java/com/stripe/model/v2/StripeCollection.java new file mode 100644 index 00000000000..dec37375d5a --- /dev/null +++ b/src/main/java/com/stripe/model/v2/StripeCollection.java @@ -0,0 +1,173 @@ +package com.stripe.model.v2; + +import com.google.gson.annotations.SerializedName; +import com.stripe.exception.StripeException; +import com.stripe.model.StripeActiveObject; +import com.stripe.model.StripeObject; +import com.stripe.model.StripeObjectInterface; +import com.stripe.net.*; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * Provides a representation of a single page worth of data from the Stripe API. + * + *

The following code will have the effect of iterating through a single page worth of invoice + * data retrieve from the API: + * + *

+ * + *

{@code
+ * foreach (Invoice invoice : Invoice.list(...).getData()) {
+ *   System.out.println("Current invoice = " + invoice.toString());
+ * }
+ * }
+ * + *

The class also provides a helper for iterating over collections that may be longer than a + * single page: + * + *

+ * + *

{@code
+ * foreach (Invoice invoice : Invoice.list(...).autoPagingIterable()) {
+ *   System.out.println("Current invoice = " + invoice.toString());
+ * }
+ * }
+ */ +@EqualsAndHashCode(callSuper = false) +public class StripeCollection extends StripeObject + implements StripeActiveObject { + private transient StripeResponseGetter responseGetter; + + @Setter private transient Type pageTypeToken; + + @Getter + @SerializedName("data") + List data; + + @Getter + @SerializedName("next_page_url") + String nextPageUrl; + + @Getter + @SerializedName("previous_page_url") + String previousPageUrl; + + @Getter @Setter private transient RequestOptions requestOptions; + + private static class Page { + List data; + String nextPageUrl; + + Page(List data, String nextPageUrl) { + this.data = data; + this.nextPageUrl = nextPageUrl; + } + } + + /** + * An Iterable implementation that starts from the StripeCollection data and will fetch next pages + * automatically. + */ + private class PagingIterable implements Iterable { + RequestOptions options; + + public PagingIterable() { + this.options = StripeCollection.this.getRequestOptions(); + } + + public PagingIterable(RequestOptions options) { + this.options = options; + } + + private Page getPage(String nextPageUrl) throws StripeException { + if (nextPageUrl == null) { + throw new IllegalArgumentException("nextPageUrl cannot be null"); + } + + StripeCollection response = + StripeCollection.this.responseGetter.request( + new ApiRequest( + BaseAddress.API, + ApiResource.RequestMethod.GET, + nextPageUrl, + new HashMap<>(), + this.options), + StripeCollection.this.pageTypeToken); + return new Page(response.getData(), response.getNextPageUrl()); + } + + @Override + public Iterator iterator() { + return new PagingIterator( + StripeCollection.this.getData(), StripeCollection.this.getNextPageUrl()); + } + + private class PagingIterator implements Iterator { + Iterator currentDataIterator; + String nextPageUrl; + + public PagingIterator(List currentPage, String nextPageUrl) { + this.currentDataIterator = currentPage.iterator(); + this.nextPageUrl = nextPageUrl; + } + + @Override + public T next() { + if (!currentDataIterator.hasNext() && this.nextPageUrl != null) { + try { + Page p = PagingIterable.this.getPage(this.nextPageUrl); + this.currentDataIterator = p.data.iterator(); + this.nextPageUrl = p.nextPageUrl; + } catch (final Exception e) { + throw new RuntimeException("Unable to paginate", e); + } + } + return this.currentDataIterator.next(); + } + + @Override + public boolean hasNext() { + return this.currentDataIterator.hasNext() || this.nextPageUrl != null; + } + } + } + + /** + * Constructs an iterable that can be used to iterate across all objects across all pages. As page + * boundaries are encountered, the next page will be fetched automatically for continued + * iteration. + * + *

This utilizes the options from the initial list request. + */ + public Iterable autoPagingIterable() { + return new PagingIterable(); + } + + /** + * Constructs an iterable that can be used to iterate across all objects across all pages. As page + * boundaries are encountered, the next page will be fetched automatically for continued + * iteration. + * + * @param options request options (will override the options from the initial list request) + */ + public Iterable autoPagingIterable(RequestOptions options) { + return new PagingIterable(options); + } + + @Override + public void setResponseGetter(StripeResponseGetter responseGetter) { + this.responseGetter = responseGetter; + + if (this.data != null) { + for (T item : data) { + trySetResponseGetter(item, responseGetter); + } + } + } +} diff --git a/src/main/java/com/stripe/model/v2/StripeDeletedObject.java b/src/main/java/com/stripe/model/v2/StripeDeletedObject.java new file mode 100644 index 00000000000..d1967ff21a4 --- /dev/null +++ b/src/main/java/com/stripe/model/v2/StripeDeletedObject.java @@ -0,0 +1,22 @@ +package com.stripe.model.v2; + +import com.google.gson.annotations.SerializedName; +import com.stripe.model.HasId; +import com.stripe.model.StripeObject; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +public class StripeDeletedObject extends StripeObject implements HasId { + /** Unique identifier for the object. */ + @Getter(onMethod_ = {@Override}) + @SerializedName("id") + String id; + + /** String representing the object’s type. Objects of the same type share the same value. */ + @SerializedName("object") + String object; +} diff --git a/src/main/java/com/stripe/model/v2/billing/MeterEvent.java b/src/main/java/com/stripe/model/v2/billing/MeterEvent.java new file mode 100644 index 00000000000..71f069fa6d0 --- /dev/null +++ b/src/main/java/com/stripe/model/v2/billing/MeterEvent.java @@ -0,0 +1,61 @@ +// File generated from our OpenAPI spec +package com.stripe.model.v2.billing; + +import com.google.gson.annotations.SerializedName; +import com.stripe.model.StripeObject; +import java.time.Instant; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +public class MeterEvent extends StripeObject { + /** The creation time of this meter event. */ + @SerializedName("created") + Instant created; + + /** The name of the meter event. Corresponds with the {@code event_name} field on a meter. */ + @SerializedName("event_name") + String eventName; + + /** + * A unique identifier for the event. If not provided, one will be generated. We recommend using a + * globally unique identifier for this. We’ll enforce uniqueness within a rolling 24 hour period. + */ + @SerializedName("identifier") + String identifier; + + /** + * Has the value {@code true} if the object exists in live mode or the value {@code false} if the + * object exists in test mode. + */ + @SerializedName("livemode") + Boolean livemode; + + /** + * String representing the object's type. Objects of the same type share the same value of the + * object field. + * + *

Equal to {@code billing.meter_event}. + */ + @SerializedName("object") + String object; + + /** + * The payload of the event. This must contain the fields corresponding to a meter’s {@code + * customer_mapping.event_payload_key} (default is {@code stripe_customer_id}) and {@code + * value_settings.event_payload_key} (default is {@code value}). Read more about the payload. + */ + @SerializedName("payload") + Map payload; + + /** + * The time of the event. Must be within the past 35 calendar days or up to 5 minutes in the + * future. Defaults to current timestamp if not specified. + */ + @SerializedName("timestamp") + Instant timestamp; +} diff --git a/src/main/java/com/stripe/model/v2/billing/MeterEventAdjustment.java b/src/main/java/com/stripe/model/v2/billing/MeterEventAdjustment.java new file mode 100644 index 00000000000..5ae4b178f40 --- /dev/null +++ b/src/main/java/com/stripe/model/v2/billing/MeterEventAdjustment.java @@ -0,0 +1,77 @@ +// File generated from our OpenAPI spec +package com.stripe.model.v2.billing; + +import com.google.gson.annotations.SerializedName; +import com.stripe.model.HasId; +import com.stripe.model.StripeObject; +import java.time.Instant; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +public class MeterEventAdjustment extends StripeObject implements HasId { + /** Specifies which event to cancel. */ + @SerializedName("cancel") + Cancel cancel; + + /** The time the adjustment was created. */ + @SerializedName("created") + Instant created; + + /** The name of the meter event. Corresponds with the {@code event_name} field on a meter. */ + @SerializedName("event_name") + String eventName; + + /** The unique id of this meter event adjustment. */ + @Getter(onMethod_ = {@Override}) + @SerializedName("id") + String id; + + /** + * Has the value {@code true} if the object exists in live mode or the value {@code false} if the + * object exists in test mode. + */ + @SerializedName("livemode") + Boolean livemode; + + /** + * String representing the object's type. Objects of the same type share the same value of the + * object field. + * + *

Equal to {@code billing.meter_event_adjustment}. + */ + @SerializedName("object") + String object; + + /** + * Open Enum. The meter event adjustment’s status. + * + *

One of {@code complete}, or {@code pending}. + */ + @SerializedName("status") + String status; + + /** + * Open Enum. Specifies whether to cancel a single event or a range of events for a time period. + * Time period cancellation is not supported yet. + * + *

Equal to {@code cancel}. + */ + @SerializedName("type") + String type; + + @Getter + @Setter + @EqualsAndHashCode(callSuper = false) + public static class Cancel extends StripeObject { + /** + * Unique identifier for the event. You can only cancel events within 24 hours of Stripe + * receiving them. + */ + @SerializedName("identifier") + String identifier; + } +} diff --git a/src/main/java/com/stripe/model/v2/billing/MeterEventSession.java b/src/main/java/com/stripe/model/v2/billing/MeterEventSession.java new file mode 100644 index 00000000000..a8b7a31fb48 --- /dev/null +++ b/src/main/java/com/stripe/model/v2/billing/MeterEventSession.java @@ -0,0 +1,51 @@ +// File generated from our OpenAPI spec +package com.stripe.model.v2.billing; + +import com.google.gson.annotations.SerializedName; +import com.stripe.model.HasId; +import com.stripe.model.StripeObject; +import java.time.Instant; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +public class MeterEventSession extends StripeObject implements HasId { + /** + * The authentication token for this session. Use this token when calling the high-throughput + * meter event API. + */ + @SerializedName("authentication_token") + String authenticationToken; + + /** The creation time of this session. */ + @SerializedName("created") + Instant created; + + /** The time at which this session will expire. */ + @SerializedName("expires_at") + Instant expiresAt; + + /** The unique id of this auth session. */ + @Getter(onMethod_ = {@Override}) + @SerializedName("id") + String id; + + /** + * Has the value {@code true} if the object exists in live mode or the value {@code false} if the + * object exists in test mode. + */ + @SerializedName("livemode") + Boolean livemode; + + /** + * String representing the object's type. Objects of the same type share the same value of the + * object field. + * + *

Equal to {@code billing.meter_event_session}. + */ + @SerializedName("object") + String object; +} diff --git a/src/main/java/com/stripe/net/ApiMode.java b/src/main/java/com/stripe/net/ApiMode.java index dc30781d5ec..f9c7e340500 100644 --- a/src/main/java/com/stripe/net/ApiMode.java +++ b/src/main/java/com/stripe/net/ApiMode.java @@ -2,5 +2,5 @@ public enum ApiMode { V1, - OAuth + V2 } diff --git a/src/main/java/com/stripe/net/ApiRequest.java b/src/main/java/com/stripe/net/ApiRequest.java index 6c45769bf19..cc318fb9f46 100644 --- a/src/main/java/com/stripe/net/ApiRequest.java +++ b/src/main/java/com/stripe/net/ApiRequest.java @@ -19,21 +19,7 @@ private ApiRequest( Map params) { super(baseAddress, method, path, options, usage); this.params = params; - this.apiMode = ApiMode.V1; - } - - /** - * @deprecated This constructor is for backward compatibility and will be removed by Sept 30, 2024 - */ - @Deprecated - public ApiRequest( - BaseAddress baseAddress, - ApiResource.RequestMethod method, - String path, - Map params, - RequestOptions options, - ApiMode apiMode) { - this(baseAddress, method, path, options, null, params); + this.apiMode = path.startsWith("/v2") ? ApiMode.V2 : ApiMode.V1; } public ApiRequest( diff --git a/src/main/java/com/stripe/net/ApiRequestParams.java b/src/main/java/com/stripe/net/ApiRequestParams.java index b6ac6cb749f..679c5f847de 100644 --- a/src/main/java/com/stripe/net/ApiRequestParams.java +++ b/src/main/java/com/stripe/net/ApiRequestParams.java @@ -11,7 +11,7 @@ public abstract class ApiRequestParams { * Param key for an `extraParams` map. Any param/sub-param specifying a field intended to support * extra params from users should have the annotation * {@code @SerializedName(ApiRequestParams.EXTRA_PARAMS_KEY)}. Logic to handle this is in {@link - * ApiRequestParamsConverter}. + * ApiRequestParamsConverter}.t */ public static final String EXTRA_PARAMS_KEY = "_stripe_java_extra_param_key"; diff --git a/src/main/java/com/stripe/net/ApiRequestParamsConverter.java b/src/main/java/com/stripe/net/ApiRequestParamsConverter.java index 94b437d7435..79a08a054b3 100644 --- a/src/main/java/com/stripe/net/ApiRequestParamsConverter.java +++ b/src/main/java/com/stripe/net/ApiRequestParamsConverter.java @@ -11,10 +11,12 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.stripe.Stripe; +import com.stripe.model.InstantSerializer; import com.stripe.param.common.EmptyParam; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.time.Instant; import java.util.Map; /** @@ -28,6 +30,7 @@ class ApiRequestParamsConverter { .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapterFactory(new HasEmptyEnumTypeAdapterFactory()) .registerTypeAdapterFactory(new NullValuesInMapsTypeAdapterFactory()) + .registerTypeAdapter(Instant.class, new InstantSerializer()) .create(); private static final UntypedMapDeserializer FLATTENING_EXTRA_PARAMS_DESERIALIZER = diff --git a/src/main/java/com/stripe/net/ApiResource.java b/src/main/java/com/stripe/net/ApiResource.java index d9a4a85888e..214803bcca7 100644 --- a/src/main/java/com/stripe/net/ApiResource.java +++ b/src/main/java/com/stripe/net/ApiResource.java @@ -3,25 +3,29 @@ import com.google.gson.*; import com.stripe.exception.InvalidRequestException; import com.stripe.model.*; +import com.stripe.model.v2.EventTypeAdapterFactory; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Objects; public abstract class ApiResource extends StripeObject implements StripeActiveObject { public static final Charset CHARSET = StandardCharsets.UTF_8; + private static StripeResponseGetter globalResponseGetter = new LiveStripeResponseGetter(); + private transient StripeResponseGetter responseGetter; public static final Gson INTERNAL_GSON = createGson(false); public static final Gson GSON = createGson(true); - public static void setStripeResponseGetter(StripeResponseGetter srg) { + public static void setGlobalResponseGetter(StripeResponseGetter srg) { ApiResource.globalResponseGetter = srg; } - protected static StripeResponseGetter getGlobalResponseGetter() { + public static StripeResponseGetter getGlobalResponseGetter() { return ApiResource.globalResponseGetter; } @@ -51,6 +55,8 @@ private static Gson createGson(boolean shouldSetResponseGetter) { .registerTypeAdapter(Event.Data.class, new EventDataDeserializer()) .registerTypeAdapter(Event.Request.class, new EventRequestDeserializer()) .registerTypeAdapter(ExpandableField.class, new ExpandableFieldDeserializer()) + .registerTypeAdapter(Instant.class, new InstantDeserializer()) + .registerTypeAdapterFactory(new EventTypeAdapterFactory()) .registerTypeAdapter(StripeRawJsonObject.class, new StripeRawJsonObjectDeserializer()) .registerTypeAdapterFactory(new StripeCollectionItemTypeSettingFactory()) .addReflectionAccessFilter( diff --git a/src/main/java/com/stripe/net/Authenticator.java b/src/main/java/com/stripe/net/Authenticator.java new file mode 100644 index 00000000000..311e6860868 --- /dev/null +++ b/src/main/java/com/stripe/net/Authenticator.java @@ -0,0 +1,15 @@ +package com.stripe.net; + +import com.stripe.exception.StripeException; + +/** * Represents a request authentication mechanism. */ +public interface Authenticator { + /** + * * Authenticate the request + * + * @param request the request that need authentication. + * @return the request with authentication headers applied. + * @throws StripeException on authentication errors. + */ + StripeRequest authenticate(StripeRequest request) throws StripeException; +} diff --git a/src/main/java/com/stripe/net/BaseAddress.java b/src/main/java/com/stripe/net/BaseAddress.java index 8151df2df21..b241f5f0ab3 100644 --- a/src/main/java/com/stripe/net/BaseAddress.java +++ b/src/main/java/com/stripe/net/BaseAddress.java @@ -7,5 +7,7 @@ public enum BaseAddress { /** https://connect.stripe.com */ CONNECT, /** https://files.stripe.com */ - FILES + FILES, + /** https://events.stripe.com */ + METER_EVENTS } diff --git a/src/main/java/com/stripe/net/BearerTokenAuthenticator.java b/src/main/java/com/stripe/net/BearerTokenAuthenticator.java new file mode 100644 index 00000000000..03c9faa8d8b --- /dev/null +++ b/src/main/java/com/stripe/net/BearerTokenAuthenticator.java @@ -0,0 +1,46 @@ +package com.stripe.net; + +import com.stripe.exception.AuthenticationException; +import com.stripe.exception.StripeException; +import com.stripe.util.StringUtils; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode +public final class BearerTokenAuthenticator implements Authenticator { + private final String apiKey; + + public BearerTokenAuthenticator(String apiKey) { + if (apiKey == null) { + throw new IllegalArgumentException("apiKey should be not-null"); + } + this.apiKey = apiKey; + } + + public String getApiKey() { + return this.apiKey; + } + + @Override + public StripeRequest authenticate(StripeRequest request) throws StripeException { + if (apiKey.isEmpty()) { + throw new AuthenticationException( + "Your API key is invalid, as it is an empty string. You can double-check your API key " + + "from the Stripe Dashboard. See " + + "https://stripe.com/docs/api/authentication for details or contact support at " + + "https://support.stripe.com/email if you have any questions.", + null, + null, + 0); + } else if (StringUtils.containsWhitespace(apiKey)) { + throw new AuthenticationException( + "Your API key is invalid, as it contains whitespace. You can double-check your API key " + + "from the Stripe Dashboard. See " + + "https://stripe.com/docs/api/authentication for details or contact support at " + + "https://support.stripe.com/email if you have any questions.", + null, + null, + 0); + } + return request.withAdditionalHeader("Authorization", String.format("Bearer %s", apiKey)); + } +} diff --git a/src/main/java/com/stripe/net/FormEncoder.java b/src/main/java/com/stripe/net/FormEncoder.java index cb12a663972..d05e4be6ee3 100644 --- a/src/main/java/com/stripe/net/FormEncoder.java +++ b/src/main/java/com/stripe/net/FormEncoder.java @@ -22,7 +22,7 @@ public static HttpContent createHttpContent(Map params) throws I return HttpContent.buildFormURLEncodedContent(new ArrayList>()); } - Collection> flatParams = flattenParams(params); + Collection> flatParams = flattenParams(params, false); // If all parameters have been encoded as strings, then the content can be represented // with application/x-www-form-url-encoded encoding. Otherwise, use @@ -46,12 +46,24 @@ public static HttpContent createHttpContent(Map params) throws I * @return The query string. */ public static String createQueryString(Map params) { + return FormEncoder.createQueryString(params, false); + } + + /** + * Creates the HTTP query string for a given map of parameters. + * + * @param params The map of parameters. + * @param arraysAsRepeated Whether to encode arrays as repeated value ({@code a=1&a=2}) defaults + * to brackets encoding ({@code a[]=1,2}). + * @return The query string. + */ + public static String createQueryString(Map params, boolean arraysAsRepeated) { if (params == null) { return ""; } Collection> flatParams = - flattenParams(params).stream() + flattenParams(params, arraysAsRepeated).stream() .filter(kvp -> kvp.getValue() instanceof String) .map(kvp -> new KeyValuePair(kvp.getKey(), (String) kvp.getValue())) .collect(Collectors.toList()); @@ -105,10 +117,13 @@ public static String createQueryString( * } * * @param params The map of parameters. + * @param arraysAsRepeated Whether to encode arrays as repeated value ({@code a=1&a=2}) defaults + * to brackets encoding ({@code a[]=1,2}). * @return The flattened list of parameters. */ - public static List> flattenParams(Map params) { - return flattenParamsValue(params, null); + public static List> flattenParams( + Map params, boolean arraysAsRepeated) { + return flattenParamsValue(params, null, arraysAsRepeated); } /** @@ -142,10 +157,12 @@ private static String urlEncode(String value) { * * @param value The value for which to create the list of parameters. * @param keyPrefix The key under which new keys should be nested, if any. + * @param arraysAsRepeated Whether to encode arrays as repeated value ({@code a=1&a=2}) defaults + * to brackets encoding ({@code a[]=1,2}). * @return The list of parameters. */ private static List> flattenParamsValue( - Object value, String keyPrefix) { + Object value, String keyPrefix, boolean arraysAsRepeated) { List> flatParams = null; // I wish Java had pattern matching :( @@ -154,7 +171,7 @@ private static List> flattenParamsValue( flatParams = singleParam(keyPrefix, ""); } else if (value instanceof Map) { - flatParams = flattenParamsMap((Map) value, keyPrefix); + flatParams = flattenParamsMap((Map) value, keyPrefix, arraysAsRepeated); } else if (value instanceof String) { flatParams = singleParam(keyPrefix, value); @@ -166,12 +183,12 @@ private static List> flattenParamsValue( flatParams = singleParam(keyPrefix, value); } else if (value instanceof Collection) { - flatParams = flattenParamsCollection((Collection) value, keyPrefix); + flatParams = flattenParamsCollection((Collection) value, keyPrefix, arraysAsRepeated); } else if (value.getClass().isArray()) { Object[] array = getArrayForObject(value); Collection collection = Arrays.stream(array).collect(Collectors.toList()); - flatParams = flattenParamsCollection(collection, keyPrefix); + flatParams = flattenParamsCollection(collection, keyPrefix, arraysAsRepeated); } else if (value.getClass().isEnum()) { flatParams = @@ -194,7 +211,7 @@ private static List> flattenParamsValue( * @return The list of parameters. */ private static List> flattenParamsMap( - Map map, String keyPrefix) { + Map map, String keyPrefix, boolean arraysAsRepeated) { List> flatParams = new ArrayList>(); if (map == null) { return flatParams; @@ -206,7 +223,7 @@ private static List> flattenParamsMap( String newPrefix = newPrefix(key, keyPrefix); - flatParams.addAll(flattenParamsValue(value, newPrefix)); + flatParams.addAll(flattenParamsValue(value, newPrefix, arraysAsRepeated)); } return flatParams; @@ -219,10 +236,12 @@ private static List> flattenParamsMap( * * @param collection The collection for which to create the list of parameters. * @param keyPrefix The key under which new keys should be nested. + * @param arraysAsRepeated Whether to encode arrays as repeated value ({@code a=1&a=2}) defaults + * to brackets encoding ({@code a[]=1,2}). * @return The list of parameters. */ private static List> flattenParamsCollection( - Collection collection, String keyPrefix) { + Collection collection, String keyPrefix, boolean arraysAsRepeated) { List> flatParams = new ArrayList>(); if (collection == null) { return flatParams; @@ -230,15 +249,15 @@ private static List> flattenParamsCollection( int index = 0; for (Object value : collection) { - String newPrefix = String.format("%s[%d]", keyPrefix, index); - flatParams.addAll(flattenParamsValue(value, newPrefix)); + String newPrefix = arraysAsRepeated ? keyPrefix : String.format("%s[%d]", keyPrefix, index); + flatParams.addAll(flattenParamsValue(value, newPrefix, arraysAsRepeated)); index += 1; } /* Because application/x-www-form-urlencoded cannot represent an empty list, convention * is to take the list parameter and just set it to an empty string. (E.g. A regular * list might look like `a[0]=1&b[1]=2`. Emptying it would look like `a=`.) */ - if (flatParams.isEmpty()) { + if (!arraysAsRepeated && flatParams.isEmpty()) { flatParams.add(new KeyValuePair(keyPrefix, "")); } diff --git a/src/main/java/com/stripe/net/GlobalStripeResponseGetterOptions.java b/src/main/java/com/stripe/net/GlobalStripeResponseGetterOptions.java index 991b2cab9ca..9f9a76252a8 100644 --- a/src/main/java/com/stripe/net/GlobalStripeResponseGetterOptions.java +++ b/src/main/java/com/stripe/net/GlobalStripeResponseGetterOptions.java @@ -15,8 +15,11 @@ public class GlobalStripeResponseGetterOptions extends StripeResponseGetterOptio private GlobalStripeResponseGetterOptions() {} @Override - public String getApiKey() { - return Stripe.apiKey; + public Authenticator getAuthenticator() { + if (Stripe.apiKey == null) { + return null; + } + return new BearerTokenAuthenticator(Stripe.apiKey); } @Override @@ -63,4 +66,14 @@ public String getFilesBase() { public String getConnectBase() { return Stripe.getConnectBase(); } + + @Override + public String getMeterEventsBase() { + return Stripe.getMeterEventsBase(); + } + + @Override + public String getStripeContext() { + return null; + } } diff --git a/src/main/java/com/stripe/net/HttpContent.java b/src/main/java/com/stripe/net/HttpContent.java index bc17023c24c..1f3012ebf9e 100644 --- a/src/main/java/com/stripe/net/HttpContent.java +++ b/src/main/java/com/stripe/net/HttpContent.java @@ -7,6 +7,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.UUID; import lombok.Value; @@ -48,6 +49,20 @@ public static HttpContent buildFormURLEncodedContent( String.format("application/x-www-form-urlencoded;charset=%s", ApiResource.CHARSET)); } + /** + * Builds a new HttpContent for name/value tuples encoded using {@code + * application/x-www-form-urlencoded} MIME type. + * + * @param content the form-encoded content string + * @return the encoded HttpContent instance + * @throws IllegalArgumentException if nameValueCollection is null + */ + public static HttpContent buildFormURLEncodedContent(String content) throws IOException { + return new HttpContent( + content.getBytes(ApiResource.CHARSET), + String.format("application/x-www-form-urlencoded;charset=%s", ApiResource.CHARSET)); + } + /** The request's content, as a string. */ public String stringContent() { return new String(this.byteArrayContent, ApiResource.CHARSET); @@ -108,4 +123,15 @@ public static HttpContent buildMultipartFormDataContent( return new HttpContent( baos.toByteArray(), String.format("multipart/form-data; boundary=%s", boundary)); } + + /** + * Builds a new HttpContent for {@code application/json} MIME type. + * + * @param json the JSON value + * @return the encoded HttpContent instance + * @throws IllegalArgumentException if nameValueCollection is null + */ + public static HttpContent buildJsonContent(String json) { + return new HttpContent(json.getBytes(StandardCharsets.UTF_8), "application/json"); + } } diff --git a/src/main/java/com/stripe/net/JsonEncoder.java b/src/main/java/com/stripe/net/JsonEncoder.java new file mode 100644 index 00000000000..35de33bc6a2 --- /dev/null +++ b/src/main/java/com/stripe/net/JsonEncoder.java @@ -0,0 +1,23 @@ +package com.stripe.net; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +final class JsonEncoder { + private static final Gson BODY_GSON = + new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .serializeNulls() + .create(); + + public static HttpContent createHttpContent(Map params) throws IOException { + if (params == null) { + params = new HashMap(); + } + return HttpContent.buildJsonContent(BODY_GSON.toJson(params)); + } +} diff --git a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java index a8f52aaa54e..411ebe524d0 100644 --- a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java +++ b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java @@ -1,10 +1,12 @@ package com.stripe.net; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; import com.stripe.Stripe; import com.stripe.exception.*; +import com.stripe.exception.ApiKeyMissingException; import com.stripe.exception.oauth.InvalidClientException; import com.stripe.exception.oauth.InvalidGrantException; import com.stripe.exception.oauth.InvalidScopeException; @@ -64,6 +66,12 @@ public LiveStripeResponseGetter(HttpClient httpClient) { this(null, httpClient); } + /** + * Initializes a new instance of the {@link LiveStripeResponseGetter} class. + * + * @param options the client options instance to use + * @param httpClient the HTTP client to use + */ public LiveStripeResponseGetter(StripeResponseGetterOptions options, HttpClient httpClient) { this.options = options != null ? options : GlobalStripeResponseGetterOptions.INSTANCE; this.httpClient = (httpClient != null) ? httpClient : buildDefaultHttpClient(); @@ -75,7 +83,33 @@ private StripeRequest toStripeRequest(ApiRequest apiRequest, RequestOptions merg Optional telemetryHeaderValue = requestTelemetry.pollPayload(); StripeRequest request = - new StripeRequest(apiRequest.getMethod(), fullUrl, apiRequest.getParams(), mergedOptions); + StripeRequest.create( + apiRequest.getMethod(), + fullUrl, + apiRequest.getParams(), + mergedOptions, + apiRequest.getApiMode()); + if (telemetryHeaderValue.isPresent()) { + request = + request.withAdditionalHeader(RequestTelemetry.HEADER_NAME, telemetryHeaderValue.get()); + } + return request; + } + + private StripeRequest toRawStripeRequest(RawApiRequest apiRequest, RequestOptions mergedOptions) + throws StripeException { + + String fullUrl = fullUrl(apiRequest); + + Optional telemetryHeaderValue = requestTelemetry.pollPayload(); + StripeRequest request = + StripeRequest.createWithStringContent( + apiRequest.getMethod(), + fullUrl, + apiRequest.getRawContent(), + mergedOptions, + apiRequest.getApiMode()); + if (telemetryHeaderValue.isPresent()) { request = request.withAdditionalHeader(RequestTelemetry.HEADER_NAME, telemetryHeaderValue.get()); @@ -103,14 +137,14 @@ public T request(ApiRequest apiRequest, Type t String requestId = response.requestId(); if (responseCode < 200 || responseCode >= 300) { - handleError(response); + handleError(response, apiRequest.getApiMode()); } T resource = null; try { resource = (T) ApiResource.deserializeStripeObject(responseBody, typeToken, this); } catch (JsonSyntaxException e) { - raiseMalformedJsonError(responseBody, responseCode, requestId, e); + throw makeMalformedJsonError(responseBody, responseCode, requestId, e); } if (resource instanceof StripeCollectionInterface) { @@ -118,6 +152,11 @@ public T request(ApiRequest apiRequest, Type t ((StripeCollectionInterface) resource).setRequestParams(apiRequest.getParams()); } + if (resource instanceof com.stripe.model.v2.StripeCollection) { + ((com.stripe.model.v2.StripeCollection) resource) + .setRequestOptions(apiRequest.getOptions()); + } + resource.setLastResponse(response); return resource; @@ -152,12 +191,44 @@ public InputStream requestStream(ApiRequest apiRequest) throws StripeException { Stripe.getApiBase(), e.getMessage()), e); } - handleError(response); + handleError(response, apiRequest.getApiMode()); } return responseStream.body(); } + @Override + public StripeResponse rawRequest(RawApiRequest apiRequest) throws StripeException { + RequestOptions mergedOptions = RequestOptions.merge(this.options, apiRequest.getOptions()); + + if (RequestOptions.unsafeGetStripeVersionOverride(mergedOptions) != null) { + apiRequest = apiRequest.addUsage("unsafe_stripe_version_override"); + } + + StripeRequest request = toRawStripeRequest(apiRequest, mergedOptions); + + Map additionalHeaders = apiRequest.getOptions().getAdditionalHeaders(); + + if (additionalHeaders != null) { + for (Map.Entry entry : additionalHeaders.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + request = request.withAdditionalHeader(key, value); + } + } + + StripeResponse response = + sendWithTelemetry(request, apiRequest.getUsage(), r -> httpClient.requestWithRetries(r)); + + int responseCode = response.code(); + + if (responseCode < 200 || responseCode >= 300) { + handleError(response, apiRequest.getApiMode()); + } + + return response; + } + @Override @SuppressWarnings({"TypeParameterUnusedInFormals", "deprecation"}) public T request( @@ -189,7 +260,7 @@ private static HttpClient buildDefaultHttpClient() { return new HttpURLConnectionClient(); } - private static void raiseMalformedJsonError( + private static ApiException makeMalformedJsonError( String responseBody, int responseCode, String requestId, Throwable e) throws ApiException { String details = e == null ? "none" : e.getMessage(); throw new ApiException( @@ -202,7 +273,22 @@ private static void raiseMalformedJsonError( e); } - private void handleError(StripeResponse response) throws StripeException { + private StripeError parseStripeError( + String body, int code, String requestId, Class klass) + throws StripeException { + StripeError ret; + try { + JsonObject jsonObject = + ApiResource.GSON.fromJson(body, JsonObject.class).getAsJsonObject("error"); + ret = (StripeError) StripeObject.deserializeStripeObject(jsonObject, klass, this); + if (ret != null) return ret; + } catch (JsonSyntaxException e) { + throw makeMalformedJsonError(body, code, requestId, e); + } + throw makeMalformedJsonError(body, code, requestId, null); + } + + private void handleError(StripeResponse response, ApiMode apiMode) throws StripeException { JsonObject responseBody = ApiResource.GSON.fromJson(response.body(), JsonObject.class); /* @@ -215,37 +301,31 @@ private void handleError(StripeResponse response) throws StripeException { if (error.isString()) { handleOAuthError(response); } + } else if (apiMode == ApiMode.V2) { + handleV2ApiError(response); } else { - handleApiError(response); + handleV1ApiError(response); } } - private void handleApiError(StripeResponse response) throws StripeException { - StripeError error = null; + private void handleV1ApiError(StripeResponse response) throws StripeException { StripeException exception = null; - try { - JsonObject jsonObject = - ApiResource.INTERNAL_GSON - .fromJson(response.body(), JsonObject.class) - .getAsJsonObject("error"); - error = ApiResource.deserializeStripeObject(jsonObject.toString(), StripeError.class, this); - } catch (JsonSyntaxException e) { - raiseMalformedJsonError(response.body(), response.code(), response.requestId(), e); - } - if (error == null) { - raiseMalformedJsonError(response.body(), response.code(), response.requestId(), null); - } + StripeError error = + parseStripeError(response.body(), response.code(), response.requestId(), StripeError.class); error.setLastResponse(response); - switch (response.code()) { case 400: case 404: if ("idempotency_error".equals(error.getType())) { exception = - new IdempotencyException( - error.getMessage(), response.requestId(), error.getCode(), response.code()); + StripeException.parseV2Exception( + "idempotency_error", + ApiResource.GSON.fromJson(response.body(), JsonObject.class), + response.code(), + response.requestId(), + this); } else { exception = new InvalidRequestException( @@ -295,23 +375,59 @@ private void handleApiError(StripeResponse response) throws StripeException { error.getMessage(), response.requestId(), error.getCode(), response.code(), null); break; } - exception.setStripeError(error); throw exception; } + private void handleV2ApiError(StripeResponse response) throws StripeException { + JsonObject body = + ApiResource.GSON.fromJson(response.body(), JsonObject.class).getAsJsonObject("error"); + + JsonElement typeElement = body == null ? null : body.get("type"); + JsonElement codeElement = body == null ? null : body.get("code"); + String type = typeElement == null ? "" : typeElement.getAsString(); + String code = codeElement == null ? "" : codeElement.getAsString(); + + StripeException exception = + StripeException.parseV2Exception(type, body, response.code(), response.requestId(), this); + if (exception != null) { + throw exception; + } + + StripeError error; + try { + error = + parseStripeError( + response.body(), response.code(), response.requestId(), StripeError.class); + } catch (ApiException e) { + String message = "Unrecognized error type '" + type + "'"; + JsonElement messageField = body == null ? null : body.get("message"); + if (messageField != null && messageField.isJsonPrimitive()) { + message = messageField.getAsString(); + } + + throw new ApiException(message, response.requestId(), code, response.code(), null); + } + + error.setLastResponse(response); + exception = + new ApiException(error.getMessage(), response.requestId(), code, response.code(), null); + exception.setStripeError(error); + throw exception; + } + private void handleOAuthError(StripeResponse response) throws StripeException { OAuthError error = null; StripeException exception = null; try { - error = ApiResource.deserializeStripeObject(response.body(), OAuthError.class, this); + error = StripeObject.deserializeStripeObject(response.body(), OAuthError.class, this); } catch (JsonSyntaxException e) { - raiseMalformedJsonError(response.body(), response.code(), response.requestId(), e); + throw makeMalformedJsonError(response.body(), response.code(), response.requestId(), e); } if (error == null) { - raiseMalformedJsonError(response.body(), response.code(), response.requestId(), null); + throw makeMalformedJsonError(response.body(), response.code(), response.requestId(), null); } error.setLastResponse(response); @@ -364,7 +480,8 @@ private void handleOAuthError(StripeResponse response) throws StripeException { @Override public void validateRequestOptions(RequestOptions options) { - if ((options == null || options.getApiKey() == null) && this.options.getApiKey() == null) { + if ((options == null || options.getAuthenticator() == null) + && this.options.getAuthenticator() == null) { throw new ApiKeyMissingException( "API key is not set. You can set the API key globally using Stripe.ApiKey, or by passing RequestOptions"); } @@ -385,6 +502,9 @@ private String fullUrl(BaseApiRequest apiRequest) { case FILES: baseUrl = this.options.getFilesBase(); break; + case METER_EVENTS: + baseUrl = this.options.getMeterEventsBase(); + break; default: throw new IllegalArgumentException("Unknown base address " + baseAddress); } diff --git a/src/main/java/com/stripe/net/OAuth.java b/src/main/java/com/stripe/net/OAuth.java index 31d979e8acf..776345ad60f 100644 --- a/src/main/java/com/stripe/net/OAuth.java +++ b/src/main/java/com/stripe/net/OAuth.java @@ -63,7 +63,6 @@ public static DeauthorizedAccount deauthorize(Map params, Reques throws StripeException { Map paramsCopy = new HashMap<>(); paramsCopy.putAll(params); - paramsCopy.put("client_id", getClientId(paramsCopy, options)); ApiRequest request = new ApiRequest( diff --git a/src/main/java/com/stripe/net/RawApiRequest.java b/src/main/java/com/stripe/net/RawApiRequest.java new file mode 100644 index 00000000000..10ff94aafdd --- /dev/null +++ b/src/main/java/com/stripe/net/RawApiRequest.java @@ -0,0 +1,51 @@ +package com.stripe.net; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; + +public class RawApiRequest extends BaseApiRequest { + @Getter(onMethod_ = {@Override}) + private RawRequestOptions options; + + @Getter private String rawContent; + + @Getter private final ApiMode apiMode; + + private RawApiRequest( + BaseAddress baseAddress, + ApiResource.RequestMethod method, + String path, + RawRequestOptions options, + List usage, + String rawContent) { + super(baseAddress, method, path, options, usage); + this.rawContent = rawContent; + this.options = options; + this.apiMode = path.startsWith("/v2") ? ApiMode.V2 : ApiMode.V1; + } + + public RawApiRequest( + BaseAddress baseAddress, + ApiResource.RequestMethod method, + String path, + String rawContent, + RawRequestOptions options) { + this(baseAddress, method, path, options, null, rawContent); + } + + public RawApiRequest addUsage(String usage) { + List newUsage = new ArrayList<>(); + if (this.getUsage() != null) { + newUsage.addAll(this.getUsage()); + } + newUsage.add(usage); + return new RawApiRequest( + this.getBaseAddress(), + this.getMethod(), + this.getPath(), + this.getOptions(), + newUsage, + this.getRawContent()); + } +} diff --git a/src/main/java/com/stripe/net/RawRequestOptions.java b/src/main/java/com/stripe/net/RawRequestOptions.java new file mode 100644 index 00000000000..abad1823552 --- /dev/null +++ b/src/main/java/com/stripe/net/RawRequestOptions.java @@ -0,0 +1,144 @@ +package com.stripe.net; + +import java.net.PasswordAuthentication; +import java.net.Proxy; +import java.util.Map; + +public class RawRequestOptions extends RequestOptions { + private Map additionalHeaders; + + public RawRequestOptions( + Authenticator authenticator, + String clientId, + String idempotencyKey, + String stripeContext, + String stripeAccount, + String stripeVersionOverride, + String baseUrl, + Integer connectTimeout, + Integer readTimeout, + Integer maxNetworkRetries, + Proxy connectionProxy, + PasswordAuthentication proxyCredential, + Map additionalHeaders) { + super( + authenticator, + clientId, + idempotencyKey, + stripeContext, + stripeAccount, + stripeVersionOverride, + baseUrl, + connectTimeout, + readTimeout, + maxNetworkRetries, + connectionProxy, + proxyCredential); + this.additionalHeaders = additionalHeaders; + } + + public Map getAdditionalHeaders() { + return additionalHeaders; + } + + public static RawRequestOptionsBuilder builder() { + return new RawRequestOptionsBuilder(); + } + + public static final class RawRequestOptionsBuilder extends RequestOptions.RequestOptionsBuilder { + private Map additionalHeaders; + + public Map getAdditionalHeaders() { + return this.additionalHeaders; + } + + public RawRequestOptionsBuilder setAdditionalHeaders(Map additionalHeaders) { + this.additionalHeaders = additionalHeaders; + return this; + } + + @Override + public RawRequestOptionsBuilder setApiKey(String apiKey) { + super.setApiKey(apiKey); + return this; + } + + @Override + public RawRequestOptionsBuilder setClientId(String clientId) { + super.setClientId(clientId); + return this; + } + + @Override + public RawRequestOptionsBuilder setIdempotencyKey(String idempotencyKey) { + super.setIdempotencyKey(idempotencyKey); + return this; + } + + @Override + public RawRequestOptionsBuilder setStripeContext(String stripeContext) { + super.setStripeContext(stripeContext); + return this; + } + + @Override + public RawRequestOptionsBuilder setStripeAccount(String stripeAccount) { + super.setStripeAccount(stripeAccount); + return this; + } + + @Override + public RawRequestOptionsBuilder setBaseUrl(String baseUrl) { + super.setBaseUrl(baseUrl); + return this; + } + + @Override + public RawRequestOptionsBuilder setConnectTimeout(Integer timeout) { + super.setConnectTimeout(timeout); + return this; + } + + @Override + public RawRequestOptionsBuilder setReadTimeout(Integer timeout) { + super.setReadTimeout(timeout); + return this; + } + + @Override + public RawRequestOptionsBuilder setMaxNetworkRetries(Integer maxNetworkRetries) { + super.setMaxNetworkRetries(maxNetworkRetries); + return this; + } + + @Override + public RawRequestOptionsBuilder setConnectionProxy(Proxy connectionProxy) { + super.setConnectionProxy(connectionProxy); + return this; + } + + @Override + public RawRequestOptionsBuilder setProxyCredential(PasswordAuthentication proxyCredential) { + super.setProxyCredential(proxyCredential); + return this; + } + + @Override + public RawRequestOptions build() { + return new RawRequestOptions( + authenticator, + normalizeClientId(this.clientId), + normalizeIdempotencyKey(this.idempotencyKey), + normalizeStripeContext(this.stripeContext), + normalizeStripeAccount(this.stripeAccount), + normalizeStripeVersion(this.stripeVersionOverride), + normalizeBaseUrl(this.baseUrl), + connectTimeout, + readTimeout, + maxNetworkRetries, + connectionProxy, + proxyCredential, + additionalHeaders); + } + } +} diff --git a/src/main/java/com/stripe/net/RequestOptions.java b/src/main/java/com/stripe/net/RequestOptions.java index aa6519e0739..4e83d396585 100644 --- a/src/main/java/com/stripe/net/RequestOptions.java +++ b/src/main/java/com/stripe/net/RequestOptions.java @@ -8,8 +8,11 @@ @EqualsAndHashCode(callSuper = false) public class RequestOptions { - private final String apiKey; + // When adding setting here keep them in sync with settings in StripeClientOptions and + // in the RequestOptions.merge method + private final Authenticator authenticator; private final String clientId; + private final String stripeContext; private final String idempotencyKey; private final String stripeAccount; private final String baseUrl; @@ -28,13 +31,15 @@ public class RequestOptions { private final PasswordAuthentication proxyCredential; public static RequestOptions getDefault() { - return new RequestOptions(null, null, null, null, null, null, null, null, null, null, null); + return new RequestOptions( + null, null, null, null, null, null, null, null, null, null, null, null); } - private RequestOptions( - String apiKey, + protected RequestOptions( + Authenticator authenticator, String clientId, String idempotencyKey, + String stripeContext, String stripeAccount, String stripeVersionOverride, String baseUrl, @@ -43,9 +48,10 @@ private RequestOptions( Integer maxNetworkRetries, Proxy connectionProxy, PasswordAuthentication proxyCredential) { - this.apiKey = apiKey; + this.authenticator = authenticator; this.clientId = clientId; this.idempotencyKey = idempotencyKey; + this.stripeContext = stripeContext; this.stripeAccount = stripeAccount; this.stripeVersionOverride = stripeVersionOverride; this.baseUrl = baseUrl; @@ -56,14 +62,26 @@ private RequestOptions( this.proxyCredential = proxyCredential; } + public Authenticator getAuthenticator() { + return this.authenticator; + } + public String getApiKey() { - return apiKey; + if (authenticator instanceof BearerTokenAuthenticator) { + return ((BearerTokenAuthenticator) authenticator).getApiKey(); + } + + return null; } public String getClientId() { return clientId; } + public String getStripeContext() { + return stripeContext; + } + public String getIdempotencyKey() { return idempotencyKey; } @@ -117,7 +135,9 @@ public static RequestOptionsBuilder builder() { */ @Deprecated public RequestOptionsBuilder toBuilder() { - return new RequestOptionsBuilder().setApiKey(this.apiKey).setStripeAccount(this.stripeAccount); + return new RequestOptionsBuilder() + .setAuthenticator(this.authenticator) + .setStripeAccount(this.stripeAccount); } /** @@ -128,7 +148,7 @@ public RequestOptionsBuilder toBuilder() { public RequestOptionsBuilder toBuilderFullCopy() { return RequestOptionsBuilder.unsafeSetStripeVersionOverride( new RequestOptionsBuilder() - .setApiKey(this.apiKey) + .setAuthenticator(this.authenticator) .setBaseUrl(this.baseUrl) .setClientId(this.clientId) .setIdempotencyKey(this.idempotencyKey) @@ -141,18 +161,19 @@ public RequestOptionsBuilder toBuilderFullCopy() { stripeVersionOverride); } - public static final class RequestOptionsBuilder { - private String apiKey; - private String clientId; - private String idempotencyKey; - private String stripeAccount; - private String stripeVersionOverride; - private Integer connectTimeout; - private Integer readTimeout; - private Integer maxNetworkRetries; - private Proxy connectionProxy; - private PasswordAuthentication proxyCredential; - private String baseUrl; + public static class RequestOptionsBuilder { + protected Authenticator authenticator; + protected String clientId; + protected String idempotencyKey; + protected String stripeContext; + protected String stripeAccount; + protected String stripeVersionOverride; + protected Integer connectTimeout; + protected Integer readTimeout; + protected Integer maxNetworkRetries; + protected Proxy connectionProxy; + protected PasswordAuthentication proxyCredential; + protected String baseUrl; /** * Constructs a request options builder with the global parameters (API key and client ID) as @@ -160,17 +181,34 @@ public static final class RequestOptionsBuilder { */ public RequestOptionsBuilder() {} + public Authenticator getAuthenticator() { + return this.authenticator; + } + + public RequestOptionsBuilder setAuthenticator(Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + public String getApiKey() { - return apiKey; + if (authenticator instanceof BearerTokenAuthenticator) { + return ((BearerTokenAuthenticator) authenticator).getApiKey(); + } + + return null; } public RequestOptionsBuilder setApiKey(String apiKey) { - this.apiKey = normalizeApiKey(apiKey); + if (apiKey == null) { + this.authenticator = null; + } else { + this.authenticator = new BearerTokenAuthenticator(normalizeApiKey(apiKey)); + } return this; } public RequestOptionsBuilder clearApiKey() { - this.apiKey = null; + this.authenticator = null; return this; } @@ -188,12 +226,26 @@ public RequestOptionsBuilder clearClientId() { return this; } + public String getStripeContext() { + return stripeContext; + } + + public RequestOptionsBuilder setStripeContext(String context) { + this.stripeContext = context; + return this; + } + + public RequestOptionsBuilder clearStripeContext() { + this.stripeContext = null; + return this; + } + public RequestOptionsBuilder setIdempotencyKey(String idempotencyKey) { this.idempotencyKey = idempotencyKey; return this; } - public int getConnectTimeout() { + public Integer getConnectTimeout() { return connectTimeout; } @@ -304,9 +356,10 @@ public RequestOptionsBuilder setBaseUrl(final String baseUrl) { /** Constructs a {@link RequestOptions} with the specified values. */ public RequestOptions build() { return new RequestOptions( - normalizeApiKey(this.apiKey), + this.authenticator, normalizeClientId(this.clientId), normalizeIdempotencyKey(this.idempotencyKey), + stripeContext, normalizeStripeAccount(this.stripeAccount), normalizeStripeVersion(this.stripeVersionOverride), normalizeBaseUrl(this.baseUrl), @@ -318,7 +371,7 @@ public RequestOptions build() { } } - private static String normalizeApiKey(String apiKey) { + protected static String normalizeApiKey(String apiKey) { // null apiKeys are considered "valid" if (apiKey == null) { return null; @@ -326,7 +379,7 @@ private static String normalizeApiKey(String apiKey) { return apiKey.trim(); } - private static String normalizeClientId(String clientId) { + protected static String normalizeClientId(String clientId) { // null client_ids are considered "valid" if (clientId == null) { return null; @@ -338,7 +391,7 @@ private static String normalizeClientId(String clientId) { return normalized; } - private static String normalizeStripeVersion(String stripeVersion) { + protected static String normalizeStripeVersion(String stripeVersion) { // null stripeVersions are considered "valid" and use Stripe.apiVersion if (stripeVersion == null) { return null; @@ -350,7 +403,7 @@ private static String normalizeStripeVersion(String stripeVersion) { return normalized; } - private static String normalizeBaseUrl(String baseUrl) { + protected static String normalizeBaseUrl(String baseUrl) { // null baseUrl is valid, and will fall back to e.g. Stripe.apiBase or Stripe.connectBase // (depending on the method) if (baseUrl == null) { @@ -363,7 +416,7 @@ private static String normalizeBaseUrl(String baseUrl) { return normalized; } - private static String normalizeIdempotencyKey(String idempotencyKey) { + protected static String normalizeIdempotencyKey(String idempotencyKey) { if (idempotencyKey == null) { return null; } @@ -380,7 +433,18 @@ private static String normalizeIdempotencyKey(String idempotencyKey) { return normalized; } - private static String normalizeStripeAccount(String stripeAccount) { + protected static String normalizeStripeContext(String stripContext) { + if (stripContext == null) { + return null; + } + String normalized = stripContext.trim(); + if (normalized.isEmpty()) { + throw new InvalidRequestOptionsException("Empty stripe context specified!"); + } + return normalized; + } + + protected static String normalizeStripeAccount(String stripeAccount) { if (stripeAccount == null) { return null; } @@ -394,9 +458,10 @@ private static String normalizeStripeAccount(String stripeAccount) { static RequestOptions merge(StripeResponseGetterOptions clientOptions, RequestOptions options) { if (options == null) { return new RequestOptions( - clientOptions.getApiKey(), // authenticator + clientOptions.getAuthenticator(), // authenticator clientOptions.getClientId(), // clientId null, // idempotencyKey + clientOptions.getStripeContext(), // stripeContext null, // stripeAccount null, // stripeVersionOverride null, // baseUrl @@ -408,9 +473,14 @@ static RequestOptions merge(StripeResponseGetterOptions clientOptions, RequestOp ); } return new RequestOptions( - options.getApiKey() != null ? options.getApiKey() : clientOptions.getApiKey(), + options.getAuthenticator() != null + ? options.getAuthenticator() + : clientOptions.getAuthenticator(), options.getClientId() != null ? options.getClientId() : clientOptions.getClientId(), options.getIdempotencyKey(), + options.getStripeContext() != null + ? options.getStripeContext() + : clientOptions.getStripeContext(), options.getStripeAccount(), RequestOptions.unsafeGetStripeVersionOverride(options), options.getBaseUrl(), diff --git a/src/main/java/com/stripe/net/RequestSigningAuthenticator.java b/src/main/java/com/stripe/net/RequestSigningAuthenticator.java new file mode 100644 index 00000000000..5b038176347 --- /dev/null +++ b/src/main/java/com/stripe/net/RequestSigningAuthenticator.java @@ -0,0 +1,167 @@ +package com.stripe.net; + +import com.stripe.exception.AuthenticationException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.List; + +public abstract class RequestSigningAuthenticator implements Authenticator { + @FunctionalInterface + interface CurrentTimeInSecondsGetter { + Long getCurrentTimeInSeconds(); + } + + private CurrentTimeInSecondsGetter currentTimeInSecondsGetter = + new CurrentTimeInSecondsGetter() { + @Override + public Long getCurrentTimeInSeconds() { + return java.time.Clock.systemUTC().millis() / 1000; + } + }; + private static final String authorizationHeaderName = "Authorization"; + private static final String stripeContextHeaderName = "Stripe-Context"; + private static final String stripeAccountHeaderName = "Stripe-Account"; + private static final String contentDigestHeaderName = "Content-Digest"; + private static final String signatureInputHeaderName = "Signature-Input"; + private static final String signatureHeaderName = "Signature"; + private static final String[] coveredHeaders = + new String[] { + "Content-Type", + contentDigestHeaderName, + stripeContextHeaderName, + stripeAccountHeaderName, + authorizationHeaderName + }; + + private static final String[] coveredHeadersGet = + new String[] {stripeContextHeaderName, stripeAccountHeaderName, authorizationHeaderName}; + + private static final String coveredHeaderFormatted; + private static final String coveredHeaderGetFormatted; + + static { + coveredHeaderFormatted = formatCoveredHeaders(coveredHeaders); + coveredHeaderGetFormatted = formatCoveredHeaders(coveredHeadersGet); + } + + private final String keyId; + + public RequestSigningAuthenticator(String keyId) { + this.keyId = keyId; + } + + @Override + public final StripeRequest authenticate(StripeRequest request) throws AuthenticationException { + if (request.content() != null) { + request = + request.withAdditionalHeader( + contentDigestHeaderName, calculateDigestHeader(request.content())); + } + + final Long created = this.currentTimeInSecondsGetter.getCurrentTimeInSeconds(); + request = + request + .withAdditionalHeader(authorizationHeaderName, String.format("STRIPE-V2-SIG %s", keyId)) + .withAdditionalHeader( + signatureInputHeaderName, + String.format("sig1=%s", calculateSignatureInput(request.method(), created))); + + final byte[] signatureBase = calculateSignatureBase(request, created); + String signature; + + try { + signature = Base64.getEncoder().encodeToString(sign(signatureBase)); + } catch (GeneralSecurityException e) { + throw new AuthenticationException("Error calculating request signature.", null, null, 0, e); + } + request = + request.withAdditionalHeader(signatureHeaderName, String.format("sig1=:%s:", signature)); + + return request; + } + + public abstract byte[] sign(byte[] signatureBase) throws GeneralSecurityException; + + RequestSigningAuthenticator withCurrentTimeInSecondsGetter(CurrentTimeInSecondsGetter getter) { + this.currentTimeInSecondsGetter = getter; + return this; + } + + private String calculateDigestHeader(HttpContent content) { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException( + "Error calculating request digest: your Java installation does not provide the SHA-256 digest algorithm, which is necessary for sending secure requests to Stripe.", + e); + } + + String digest = + Base64.getEncoder().encodeToString(messageDigest.digest(content.byteArrayContent())); + return String.format("sha-256=:%s:", digest); + } + + private byte[] calculateSignatureBase(StripeRequest request, Long created) { + StringBuilder stringBuilder = new StringBuilder(); + String[] headers = + request.method() == ApiResource.RequestMethod.GET ? coveredHeadersGet : coveredHeaders; + for (String header : headers) { + List values = request.headers().allValues(header); + + stringBuilder.append('"').append(header.toLowerCase()).append("\": "); + boolean firstValue = true; + for (String value : values) { + if (firstValue) { + firstValue = false; + } else { + stringBuilder.append(","); + } + stringBuilder.append(value); + } + + stringBuilder.append('\n'); + } + + stringBuilder.append("\"@signature-params\": "); + appendSignatureInput(stringBuilder, request.method(), created); + + return stringBuilder.toString().getBytes(StandardCharsets.UTF_8); + } + + private String calculateSignatureInput(ApiResource.RequestMethod method, Long created) { + StringBuilder stringBuilder = new StringBuilder(); + appendSignatureInput(stringBuilder, method, created); + return stringBuilder.toString(); + } + + private void appendSignatureInput( + StringBuilder stringBuilder, ApiResource.RequestMethod method, Long created) { + stringBuilder + .append( + method == ApiResource.RequestMethod.GET + ? coveredHeaderGetFormatted + : coveredHeaderFormatted) + .append(";created=") + .append(created); + } + + private static String formatCoveredHeaders(String[] headers) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append('('); + boolean first = true; + for (String header : headers) { + if (first) { + first = false; + } else { + stringBuilder.append(' '); + } + stringBuilder.append('"').append(header.toLowerCase()).append('"'); + } + stringBuilder.append(')'); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/com/stripe/net/StripeCollectionItemTypeSettingFactory.java b/src/main/java/com/stripe/net/StripeCollectionItemTypeSettingFactory.java index 8101ceb2f06..c881215e1cb 100644 --- a/src/main/java/com/stripe/net/StripeCollectionItemTypeSettingFactory.java +++ b/src/main/java/com/stripe/net/StripeCollectionItemTypeSettingFactory.java @@ -25,6 +25,8 @@ public T read(JsonReader in) throws IOException { T obj = delegate.read(in); if (obj instanceof StripeCollectionInterface) { ((StripeCollectionInterface) obj).setPageTypeToken(type.getType()); + } else if (obj instanceof com.stripe.model.v2.StripeCollection) { + ((com.stripe.model.v2.StripeCollection) obj).setPageTypeToken(type.getType()); } return obj; } diff --git a/src/main/java/com/stripe/net/StripeRequest.java b/src/main/java/com/stripe/net/StripeRequest.java index 53acfea729a..4afd391f9c6 100644 --- a/src/main/java/com/stripe/net/StripeRequest.java +++ b/src/main/java/com/stripe/net/StripeRequest.java @@ -4,15 +4,9 @@ import com.stripe.exception.ApiConnectionException; import com.stripe.exception.AuthenticationException; import com.stripe.exception.StripeException; -import com.stripe.util.StringUtils; import java.io.IOException; import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Value; @@ -56,23 +50,26 @@ public class StripeRequest { * * @param method the HTTP method * @param url the URL of the request + * @param content the body of the request * @param params the parameters of the request * @param options the special modifiers of the request * @throws StripeException if the request cannot be initialized for any reason */ - public StripeRequest( + private StripeRequest( ApiResource.RequestMethod method, String url, + HttpContent content, Map params, - RequestOptions options) + RequestOptions options, + ApiMode apiMode) throws StripeException { try { + this.content = content; this.params = (params != null) ? Collections.unmodifiableMap(params) : null; this.options = (options != null) ? options : RequestOptions.getDefault(); this.method = method; - this.url = buildURL(method, url, params); - this.content = buildContent(method, params); - this.headers = buildHeaders(method, this.options); + this.url = buildURL(method, url, params, apiMode); + this.headers = buildHeaders(method, this.options, this.content, apiMode); } catch (IOException e) { throw new ApiConnectionException( String.format( @@ -85,6 +82,119 @@ public StripeRequest( } } + /** + * Initializes a new instance of the {@link StripeRequest} class. + * + * @param method the HTTP method + * @param url the URL of the request + * @param params the parameters of the request + * @param options the special modifiers of the request + * @param apiMode version of the API + * @throws StripeException if the request cannot be initialized for any reason + */ + StripeRequest( + ApiResource.RequestMethod method, + String url, + Map params, + RequestOptions options, + ApiMode apiMode) + throws StripeException { + try { + this.params = (params != null) ? Collections.unmodifiableMap(params) : null; + this.options = options; + this.method = method; + this.url = buildURL(method, url, params, apiMode); + this.content = buildContent(method, params, apiMode); + this.headers = buildHeaders(method, this.options, this.content, apiMode); + } catch (IOException e) { + throw new ApiConnectionException( + String.format( + "IOException during API request to Stripe (%s): %s " + + "Please check your internet connection and try again. If this problem persists," + + "you should check Stripe's service status at https://twitter.com/stripestatus," + + " or let us know at support@stripe.com.", + Stripe.getApiBase(), e.getMessage()), + e); + } + } + + /** + * Initializes a new instance of the {@link StripeRequest} class. + * + * @param method the HTTP method + * @param url the URL of the request + * @param params the parameters of the request + * @param options the special modifiers of the request + * @throws StripeException if the request cannot be initialized for any reason + */ + public static StripeRequest create( + ApiResource.RequestMethod method, + String url, + Map params, + RequestOptions options, + ApiMode apiMode) + throws StripeException { + if (options == null) { + throw new IllegalArgumentException("options parameter should not be null"); + } + + StripeRequest request = new StripeRequest(method, url, params, options, apiMode); + Authenticator authenticator = options.getAuthenticator(); + + if (authenticator == null) { + throw new AuthenticationException( + "No API key provided. Set your API key using `Stripe.apiKey = \"\"`. You can " + + "generate API keys from the Stripe Dashboard. See " + + "https://stripe.com/docs/api/authentication for details or contact support at " + + "https://support.stripe.com/email if you have any questions.", + null, + null, + 0); + } + + request = request.options().getAuthenticator().authenticate(request); + + return request; + } + + /** + * Initializes a new instance of the {@link StripeRequest} class. + * + * @param method the HTTP method + * @param url the URL of the request + * @param content the body of the request + * @param options the special modifiers of the request + * @throws StripeException if the request cannot be initialized for any reason + */ + public static StripeRequest createWithStringContent( + ApiResource.RequestMethod method, + String url, + String content, + RequestOptions options, + ApiMode apiMode) + throws StripeException { + StripeRequest request = + new StripeRequest( + method, url, buildContentFromString(method, content, apiMode), null, options, apiMode); + + Authenticator authenticator = options.getAuthenticator(); + + if (authenticator == null) { + throw new AuthenticationException( + "No API key provided. Set your API key using `Stripe.apiKey = \"\"`. You can " + + "generate API keys from the Stripe Dashboard. See " + + "https://stripe.com/docs/api/authentication for details or contact support at " + + "https://support.stripe.com/email if you have any questions.", + null, + null, + 0); + } + + request = request.options().getAuthenticator().authenticate(request); + + return request; + } + /** * Returns a new {@link StripeRequest} instance with an additional header. * @@ -103,7 +213,7 @@ public StripeRequest withAdditionalHeader(String name, String value) { } private static URL buildURL( - ApiResource.RequestMethod method, String spec, Map params) + ApiResource.RequestMethod method, String spec, Map params, ApiMode apiMode) throws IOException { StringBuilder sb = new StringBuilder(); @@ -113,7 +223,8 @@ private static URL buildURL( String specQueryString = specUrl.getQuery(); if ((method != ApiResource.RequestMethod.POST) && (params != null)) { - String queryString = FormEncoder.createQueryString(params); + String queryString = + FormEncoder.createQueryString(params, apiMode == ApiMode.V2 ? true : false); if (queryString != null && !queryString.isEmpty()) { if (specQueryString != null && !specQueryString.isEmpty()) { @@ -129,16 +240,55 @@ private static URL buildURL( } private static HttpContent buildContent( - ApiResource.RequestMethod method, Map params) throws IOException { + ApiResource.RequestMethod method, Map params, ApiMode apiMode) + throws IOException { if (method != ApiResource.RequestMethod.POST) { return null; } + if (apiMode == ApiMode.V2) { + return JsonEncoder.createHttpContent(params); + } + return FormEncoder.createHttpContent(params); } - private static HttpHeaders buildHeaders(ApiResource.RequestMethod method, RequestOptions options) - throws AuthenticationException { + private static HttpContent buildContentFromString( + ApiResource.RequestMethod method, String content, ApiMode apiMode) + throws ApiConnectionException { + if (method != ApiResource.RequestMethod.POST) { + return null; + } + + if (apiMode == ApiMode.V2) { + return HttpContent.buildJsonContent(content); + } + + HttpContent httpContent = null; + try { + httpContent = HttpContent.buildFormURLEncodedContent(content); + } catch (IOException e) { + handleIOException(e); + } + return httpContent; + } + + private static void handleIOException(IOException e) throws ApiConnectionException { + throw new ApiConnectionException( + String.format( + "IOException during API request to Stripe (%s): %s " + + "Please check your internet connection and try again. If this problem persists," + + "you should check Stripe's service status at https://twitter.com/stripestatus," + + " or let us know at support@stripe.com.", + Stripe.getApiBase(), e.getMessage()), + e); + } + + private static HttpHeaders buildHeaders( + ApiResource.RequestMethod method, + RequestOptions options, + HttpContent content, + ApiMode apiMode) { Map> headerMap = new HashMap>(); // Accept @@ -147,47 +297,25 @@ private static HttpHeaders buildHeaders(ApiResource.RequestMethod method, Reques // Accept-Charset headerMap.put("Accept-Charset", Arrays.asList(ApiResource.CHARSET.name())); - // Authorization - String apiKey = options.getApiKey(); - if (apiKey == null) { - throw new AuthenticationException( - "No API key provided. Set your API key using `Stripe.apiKey = \"\"`. You can " - + "generate API keys from the Stripe Dashboard. See " - + "https://stripe.com/docs/api/authentication for details or contact support at " - + "https://support.stripe.com/email if you have any questions.", - null, - null, - 0); - } else if (apiKey.isEmpty()) { - throw new AuthenticationException( - "Your API key is invalid, as it is an empty string. You can double-check your API key " - + "from the Stripe Dashboard. See " - + "https://stripe.com/docs/api/authentication for details or contact support at " - + "https://support.stripe.com/email if you have any questions.", - null, - null, - 0); - } else if (StringUtils.containsWhitespace(apiKey)) { - throw new AuthenticationException( - "Your API key is invalid, as it contains whitespace. You can double-check your API key " - + "from the Stripe Dashboard. See " - + "https://stripe.com/docs/api/authentication for details or contact support at " - + "https://support.stripe.com/email if you have any questions.", - null, - null, - 0); - } - headerMap.put("Authorization", Arrays.asList(String.format("Bearer %s", apiKey))); - // Stripe-Version if (RequestOptions.unsafeGetStripeVersionOverride(options) != null) { headerMap.put( "Stripe-Version", Arrays.asList(RequestOptions.unsafeGetStripeVersionOverride(options))); } else if (options.getStripeVersion() != null) { headerMap.put("Stripe-Version", Arrays.asList(options.getStripeVersion())); + } + + if (apiMode == ApiMode.V1) { + if (options.getStripeContext() != null) { + throw new UnsupportedOperationException("Context is not supported in V1 APIs"); + } } else { - throw new IllegalStateException( - "Either `stripeVersion` or `stripeVersionOverride` value must be set."); + if (options.getStripeContext() != null) { + headerMap.put("Stripe-Context", Arrays.asList(options.getStripeContext())); + } + if (content != null) { + headerMap.put("Content-Type", Arrays.asList(content.contentType())); + } } // Stripe-Account @@ -198,7 +326,8 @@ private static HttpHeaders buildHeaders(ApiResource.RequestMethod method, Reques // Idempotency-Key if (options.getIdempotencyKey() != null) { headerMap.put("Idempotency-Key", Arrays.asList(options.getIdempotencyKey())); - } else if (method == ApiResource.RequestMethod.POST) { + } else if (method == ApiResource.RequestMethod.POST + || (apiMode == ApiMode.V2 && method == ApiResource.RequestMethod.DELETE)) { headerMap.put("Idempotency-Key", Arrays.asList(UUID.randomUUID().toString())); } diff --git a/src/main/java/com/stripe/net/StripeResponseGetter.java b/src/main/java/com/stripe/net/StripeResponseGetter.java index 5b27a9862c6..cb6b54cf10d 100644 --- a/src/main/java/com/stripe/net/StripeResponseGetter.java +++ b/src/main/java/com/stripe/net/StripeResponseGetter.java @@ -55,6 +55,11 @@ default InputStream requestStream(ApiRequest request) throws StripeException { request.getApiMode()); }; + default StripeResponse rawRequest(RawApiRequest request) throws StripeException { + throw new UnsupportedOperationException( + "rawRequest is unimplemented for this StripeResponseGetter"); + }; + /** * This method should e.g. throws an ApiKeyMissingError if a proper API Key cannot be determined * by the ResponseGetter or from the RequestOptions passed in. diff --git a/src/main/java/com/stripe/net/StripeResponseGetterOptions.java b/src/main/java/com/stripe/net/StripeResponseGetterOptions.java index 8aa2fbccb4a..e2121017c1d 100644 --- a/src/main/java/com/stripe/net/StripeResponseGetterOptions.java +++ b/src/main/java/com/stripe/net/StripeResponseGetterOptions.java @@ -7,7 +7,7 @@ public abstract class StripeResponseGetterOptions { // When adding settings here keep them in sync with settings in RequestOptions and // in the RequestOptions.merge method - public abstract String getApiKey(); + public abstract Authenticator getAuthenticator(); public abstract String getClientId(); @@ -25,5 +25,9 @@ public abstract class StripeResponseGetterOptions { public abstract String getConnectBase(); + public abstract String getMeterEventsBase(); + public abstract int getReadTimeout(); + + public abstract String getStripeContext(); } diff --git a/src/main/java/com/stripe/net/Webhook.java b/src/main/java/com/stripe/net/Webhook.java index 09faafc4330..7b5a34febf2 100644 --- a/src/main/java/com/stripe/net/Webhook.java +++ b/src/main/java/com/stripe/net/Webhook.java @@ -14,7 +14,7 @@ import javax.crypto.spec.SecretKeySpec; public final class Webhook { - private static final long DEFAULT_TOLERANCE = 300; + public static final long DEFAULT_TOLERANCE = 300; /** * Returns an Event instance using the provided JSON payload. Throws a JsonSyntaxException if the diff --git a/src/main/java/com/stripe/param/v2/billing/MeterEventAdjustmentCreateParams.java b/src/main/java/com/stripe/param/v2/billing/MeterEventAdjustmentCreateParams.java new file mode 100644 index 00000000000..18f0706e9bb --- /dev/null +++ b/src/main/java/com/stripe/param/v2/billing/MeterEventAdjustmentCreateParams.java @@ -0,0 +1,203 @@ +// File generated from our OpenAPI spec +package com.stripe.param.v2.billing; + +import com.google.gson.annotations.SerializedName; +import com.stripe.net.ApiRequestParams; +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; + +@Getter +public class MeterEventAdjustmentCreateParams extends ApiRequestParams { + /** Required. Specifies which event to cancel. */ + @SerializedName("cancel") + Cancel cancel; + + /** + * Required. The name of the meter event. Corresponds with the {@code event_name} + * field on a meter. + */ + @SerializedName("event_name") + String eventName; + + /** + * Map of extra parameters for custom features not available in this client library. The content + * in this map is not serialized under this field's {@code @SerializedName} value. Instead, each + * key/value pair is serialized as if the key is a root-level field (serialized) name in this + * param object. Effectively, this map is flattened to its parent instance. + */ + @SerializedName(ApiRequestParams.EXTRA_PARAMS_KEY) + Map extraParams; + + /** + * Required. Specifies whether to cancel a single event or a range of events for + * a time period. Time period cancellation is not supported yet. + */ + @SerializedName("type") + Type type; + + private MeterEventAdjustmentCreateParams( + Cancel cancel, String eventName, Map extraParams, Type type) { + this.cancel = cancel; + this.eventName = eventName; + this.extraParams = extraParams; + this.type = type; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Cancel cancel; + + private String eventName; + + private Map extraParams; + + private Type type; + + /** Finalize and obtain parameter instance from this builder. */ + public MeterEventAdjustmentCreateParams build() { + return new MeterEventAdjustmentCreateParams( + this.cancel, this.eventName, this.extraParams, this.type); + } + + /** Required. Specifies which event to cancel. */ + public Builder setCancel(MeterEventAdjustmentCreateParams.Cancel cancel) { + this.cancel = cancel; + return this; + } + + /** + * Required. The name of the meter event. Corresponds with the {@code + * event_name} field on a meter. + */ + public Builder setEventName(String eventName) { + this.eventName = eventName; + return this; + } + + /** + * Add a key/value pair to `extraParams` map. A map is initialized for the first `put/putAll` + * call, and subsequent calls add additional key/value pairs to the original map. See {@link + * MeterEventAdjustmentCreateParams#extraParams} for the field documentation. + */ + public Builder putExtraParam(String key, Object value) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.put(key, value); + return this; + } + + /** + * Add all map key/value pairs to `extraParams` map. A map is initialized for the first + * `put/putAll` call, and subsequent calls add additional key/value pairs to the original map. + * See {@link MeterEventAdjustmentCreateParams#extraParams} for the field documentation. + */ + public Builder putAllExtraParam(Map map) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.putAll(map); + return this; + } + + /** + * Required. Specifies whether to cancel a single event or a range of events + * for a time period. Time period cancellation is not supported yet. + */ + public Builder setType(MeterEventAdjustmentCreateParams.Type type) { + this.type = type; + return this; + } + } + + @Getter + public static class Cancel { + /** + * Map of extra parameters for custom features not available in this client library. The content + * in this map is not serialized under this field's {@code @SerializedName} value. Instead, each + * key/value pair is serialized as if the key is a root-level field (serialized) name in this + * param object. Effectively, this map is flattened to its parent instance. + */ + @SerializedName(ApiRequestParams.EXTRA_PARAMS_KEY) + Map extraParams; + + /** + * Required. Unique identifier for the event. You can only cancel events within + * 24 hours of Stripe receiving them. + */ + @SerializedName("identifier") + String identifier; + + private Cancel(Map extraParams, String identifier) { + this.extraParams = extraParams; + this.identifier = identifier; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Map extraParams; + + private String identifier; + + /** Finalize and obtain parameter instance from this builder. */ + public MeterEventAdjustmentCreateParams.Cancel build() { + return new MeterEventAdjustmentCreateParams.Cancel(this.extraParams, this.identifier); + } + + /** + * Add a key/value pair to `extraParams` map. A map is initialized for the first `put/putAll` + * call, and subsequent calls add additional key/value pairs to the original map. See {@link + * MeterEventAdjustmentCreateParams.Cancel#extraParams} for the field documentation. + */ + public Builder putExtraParam(String key, Object value) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.put(key, value); + return this; + } + + /** + * Add all map key/value pairs to `extraParams` map. A map is initialized for the first + * `put/putAll` call, and subsequent calls add additional key/value pairs to the original map. + * See {@link MeterEventAdjustmentCreateParams.Cancel#extraParams} for the field + * documentation. + */ + public Builder putAllExtraParam(Map map) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.putAll(map); + return this; + } + + /** + * Required. Unique identifier for the event. You can only cancel events + * within 24 hours of Stripe receiving them. + */ + public Builder setIdentifier(String identifier) { + this.identifier = identifier; + return this; + } + } + } + + public enum Type implements ApiRequestParams.EnumParam { + @SerializedName("cancel") + CANCEL("cancel"); + + @Getter(onMethod_ = {@Override}) + private final String value; + + Type(String value) { + this.value = value; + } + } +} diff --git a/src/main/java/com/stripe/param/v2/billing/MeterEventCreateParams.java b/src/main/java/com/stripe/param/v2/billing/MeterEventCreateParams.java new file mode 100644 index 00000000000..2b3655f5ac6 --- /dev/null +++ b/src/main/java/com/stripe/param/v2/billing/MeterEventCreateParams.java @@ -0,0 +1,166 @@ +// File generated from our OpenAPI spec +package com.stripe.param.v2.billing; + +import com.google.gson.annotations.SerializedName; +import com.stripe.net.ApiRequestParams; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; + +@Getter +public class MeterEventCreateParams extends ApiRequestParams { + /** + * Required. The name of the meter event. Corresponds with the {@code event_name} + * field on a meter. + */ + @SerializedName("event_name") + String eventName; + + /** + * Map of extra parameters for custom features not available in this client library. The content + * in this map is not serialized under this field's {@code @SerializedName} value. Instead, each + * key/value pair is serialized as if the key is a root-level field (serialized) name in this + * param object. Effectively, this map is flattened to its parent instance. + */ + @SerializedName(ApiRequestParams.EXTRA_PARAMS_KEY) + Map extraParams; + + /** + * A unique identifier for the event. If not provided, one will be generated. We recommend using a + * globally unique identifier for this. We’ll enforce uniqueness within a rolling 24 hour period. + */ + @SerializedName("identifier") + String identifier; + + /** + * Required. The payload of the event. This must contain the fields corresponding + * to a meter’s {@code customer_mapping.event_payload_key} (default is {@code stripe_customer_id}) + * and {@code value_settings.event_payload_key} (default is {@code value}). Read more about the payload. + */ + @SerializedName("payload") + Map payload; + + /** + * The time of the event. Must be within the past 35 calendar days or up to 5 minutes in the + * future. Defaults to current timestamp if not specified. + */ + @SerializedName("timestamp") + Instant timestamp; + + private MeterEventCreateParams( + String eventName, + Map extraParams, + String identifier, + Map payload, + Instant timestamp) { + this.eventName = eventName; + this.extraParams = extraParams; + this.identifier = identifier; + this.payload = payload; + this.timestamp = timestamp; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String eventName; + + private Map extraParams; + + private String identifier; + + private Map payload; + + private Instant timestamp; + + /** Finalize and obtain parameter instance from this builder. */ + public MeterEventCreateParams build() { + return new MeterEventCreateParams( + this.eventName, this.extraParams, this.identifier, this.payload, this.timestamp); + } + + /** + * Required. The name of the meter event. Corresponds with the {@code + * event_name} field on a meter. + */ + public Builder setEventName(String eventName) { + this.eventName = eventName; + return this; + } + + /** + * Add a key/value pair to `extraParams` map. A map is initialized for the first `put/putAll` + * call, and subsequent calls add additional key/value pairs to the original map. See {@link + * MeterEventCreateParams#extraParams} for the field documentation. + */ + public Builder putExtraParam(String key, Object value) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.put(key, value); + return this; + } + + /** + * Add all map key/value pairs to `extraParams` map. A map is initialized for the first + * `put/putAll` call, and subsequent calls add additional key/value pairs to the original map. + * See {@link MeterEventCreateParams#extraParams} for the field documentation. + */ + public Builder putAllExtraParam(Map map) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.putAll(map); + return this; + } + + /** + * A unique identifier for the event. If not provided, one will be generated. We recommend using + * a globally unique identifier for this. We’ll enforce uniqueness within a rolling 24 hour + * period. + */ + public Builder setIdentifier(String identifier) { + this.identifier = identifier; + return this; + } + + /** + * Add a key/value pair to `payload` map. A map is initialized for the first `put/putAll` call, + * and subsequent calls add additional key/value pairs to the original map. See {@link + * MeterEventCreateParams#payload} for the field documentation. + */ + public Builder putPayload(String key, String value) { + if (this.payload == null) { + this.payload = new HashMap<>(); + } + this.payload.put(key, value); + return this; + } + + /** + * Add all map key/value pairs to `payload` map. A map is initialized for the first `put/putAll` + * call, and subsequent calls add additional key/value pairs to the original map. See {@link + * MeterEventCreateParams#payload} for the field documentation. + */ + public Builder putAllPayload(Map map) { + if (this.payload == null) { + this.payload = new HashMap<>(); + } + this.payload.putAll(map); + return this; + } + + /** + * The time of the event. Must be within the past 35 calendar days or up to 5 minutes in the + * future. Defaults to current timestamp if not specified. + */ + public Builder setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + } +} diff --git a/src/main/java/com/stripe/param/v2/billing/MeterEventStreamCreateParams.java b/src/main/java/com/stripe/param/v2/billing/MeterEventStreamCreateParams.java new file mode 100644 index 00000000000..a47898d5037 --- /dev/null +++ b/src/main/java/com/stripe/param/v2/billing/MeterEventStreamCreateParams.java @@ -0,0 +1,259 @@ +// File generated from our OpenAPI spec +package com.stripe.param.v2.billing; + +import com.google.gson.annotations.SerializedName; +import com.stripe.net.ApiRequestParams; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; + +@Getter +public class MeterEventStreamCreateParams extends ApiRequestParams { + /** Required. List of meter events to include in the request. */ + @SerializedName("events") + List events; + + /** + * Map of extra parameters for custom features not available in this client library. The content + * in this map is not serialized under this field's {@code @SerializedName} value. Instead, each + * key/value pair is serialized as if the key is a root-level field (serialized) name in this + * param object. Effectively, this map is flattened to its parent instance. + */ + @SerializedName(ApiRequestParams.EXTRA_PARAMS_KEY) + Map extraParams; + + private MeterEventStreamCreateParams( + List events, Map extraParams) { + this.events = events; + this.extraParams = extraParams; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private List events; + + private Map extraParams; + + /** Finalize and obtain parameter instance from this builder. */ + public MeterEventStreamCreateParams build() { + return new MeterEventStreamCreateParams(this.events, this.extraParams); + } + + /** + * Add an element to `events` list. A list is initialized for the first `add/addAll` call, and + * subsequent calls adds additional elements to the original list. See {@link + * MeterEventStreamCreateParams#events} for the field documentation. + */ + public Builder addEvent(MeterEventStreamCreateParams.Event element) { + if (this.events == null) { + this.events = new ArrayList<>(); + } + this.events.add(element); + return this; + } + + /** + * Add all elements to `events` list. A list is initialized for the first `add/addAll` call, and + * subsequent calls adds additional elements to the original list. See {@link + * MeterEventStreamCreateParams#events} for the field documentation. + */ + public Builder addAllEvent(List elements) { + if (this.events == null) { + this.events = new ArrayList<>(); + } + this.events.addAll(elements); + return this; + } + + /** + * Add a key/value pair to `extraParams` map. A map is initialized for the first `put/putAll` + * call, and subsequent calls add additional key/value pairs to the original map. See {@link + * MeterEventStreamCreateParams#extraParams} for the field documentation. + */ + public Builder putExtraParam(String key, Object value) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.put(key, value); + return this; + } + + /** + * Add all map key/value pairs to `extraParams` map. A map is initialized for the first + * `put/putAll` call, and subsequent calls add additional key/value pairs to the original map. + * See {@link MeterEventStreamCreateParams#extraParams} for the field documentation. + */ + public Builder putAllExtraParam(Map map) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.putAll(map); + return this; + } + } + + @Getter + public static class Event { + /** + * Required. The name of the meter event. Corresponds with the {@code + * event_name} field on a meter. + */ + @SerializedName("event_name") + String eventName; + + /** + * Map of extra parameters for custom features not available in this client library. The content + * in this map is not serialized under this field's {@code @SerializedName} value. Instead, each + * key/value pair is serialized as if the key is a root-level field (serialized) name in this + * param object. Effectively, this map is flattened to its parent instance. + */ + @SerializedName(ApiRequestParams.EXTRA_PARAMS_KEY) + Map extraParams; + + /** + * A unique identifier for the event. If not provided, one will be generated. We recommend using + * a globally unique identifier for this. We’ll enforce uniqueness within a rolling 24 hour + * period. + */ + @SerializedName("identifier") + String identifier; + + /** + * Required. The payload of the event. This must contain the fields + * corresponding to a meter’s {@code customer_mapping.event_payload_key} (default is {@code + * stripe_customer_id}) and {@code value_settings.event_payload_key} (default is {@code value}). + * Read more about the payload. + */ + @SerializedName("payload") + Map payload; + + /** + * The time of the event. Must be within the past 35 calendar days or up to 5 minutes in the + * future. Defaults to current timestamp if not specified. + */ + @SerializedName("timestamp") + Instant timestamp; + + private Event( + String eventName, + Map extraParams, + String identifier, + Map payload, + Instant timestamp) { + this.eventName = eventName; + this.extraParams = extraParams; + this.identifier = identifier; + this.payload = payload; + this.timestamp = timestamp; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String eventName; + + private Map extraParams; + + private String identifier; + + private Map payload; + + private Instant timestamp; + + /** Finalize and obtain parameter instance from this builder. */ + public MeterEventStreamCreateParams.Event build() { + return new MeterEventStreamCreateParams.Event( + this.eventName, this.extraParams, this.identifier, this.payload, this.timestamp); + } + + /** + * Required. The name of the meter event. Corresponds with the {@code + * event_name} field on a meter. + */ + public Builder setEventName(String eventName) { + this.eventName = eventName; + return this; + } + + /** + * Add a key/value pair to `extraParams` map. A map is initialized for the first `put/putAll` + * call, and subsequent calls add additional key/value pairs to the original map. See {@link + * MeterEventStreamCreateParams.Event#extraParams} for the field documentation. + */ + public Builder putExtraParam(String key, Object value) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.put(key, value); + return this; + } + + /** + * Add all map key/value pairs to `extraParams` map. A map is initialized for the first + * `put/putAll` call, and subsequent calls add additional key/value pairs to the original map. + * See {@link MeterEventStreamCreateParams.Event#extraParams} for the field documentation. + */ + public Builder putAllExtraParam(Map map) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.putAll(map); + return this; + } + + /** + * A unique identifier for the event. If not provided, one will be generated. We recommend + * using a globally unique identifier for this. We’ll enforce uniqueness within a rolling 24 + * hour period. + */ + public Builder setIdentifier(String identifier) { + this.identifier = identifier; + return this; + } + + /** + * Add a key/value pair to `payload` map. A map is initialized for the first `put/putAll` + * call, and subsequent calls add additional key/value pairs to the original map. See {@link + * MeterEventStreamCreateParams.Event#payload} for the field documentation. + */ + public Builder putPayload(String key, String value) { + if (this.payload == null) { + this.payload = new HashMap<>(); + } + this.payload.put(key, value); + return this; + } + + /** + * Add all map key/value pairs to `payload` map. A map is initialized for the first + * `put/putAll` call, and subsequent calls add additional key/value pairs to the original map. + * See {@link MeterEventStreamCreateParams.Event#payload} for the field documentation. + */ + public Builder putAllPayload(Map map) { + if (this.payload == null) { + this.payload = new HashMap<>(); + } + this.payload.putAll(map); + return this; + } + + /** + * The time of the event. Must be within the past 35 calendar days or up to 5 minutes in the + * future. Defaults to current timestamp if not specified. + */ + public Builder setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + } + } +} diff --git a/src/main/java/com/stripe/param/v2/core/EventListParams.java b/src/main/java/com/stripe/param/v2/core/EventListParams.java new file mode 100644 index 00000000000..a7a871fcff1 --- /dev/null +++ b/src/main/java/com/stripe/param/v2/core/EventListParams.java @@ -0,0 +1,99 @@ +// File generated from our OpenAPI spec +package com.stripe.param.v2.core; + +import com.google.gson.annotations.SerializedName; +import com.stripe.net.ApiRequestParams; +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; + +@Getter +public class EventListParams extends ApiRequestParams { + /** + * Map of extra parameters for custom features not available in this client library. The content + * in this map is not serialized under this field's {@code @SerializedName} value. Instead, each + * key/value pair is serialized as if the key is a root-level field (serialized) name in this + * param object. Effectively, this map is flattened to its parent instance. + */ + @SerializedName(ApiRequestParams.EXTRA_PARAMS_KEY) + Map extraParams; + + @SerializedName("limit") + Integer limit; + + /** Required. Primary object ID used to retrieve related events. */ + @SerializedName("object_id") + String objectId; + + @SerializedName("page") + String page; + + private EventListParams( + Map extraParams, Integer limit, String objectId, String page) { + this.extraParams = extraParams; + this.limit = limit; + this.objectId = objectId; + this.page = page; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Map extraParams; + + private Integer limit; + + private String objectId; + + private String page; + + /** Finalize and obtain parameter instance from this builder. */ + public EventListParams build() { + return new EventListParams(this.extraParams, this.limit, this.objectId, this.page); + } + + /** + * Add a key/value pair to `extraParams` map. A map is initialized for the first `put/putAll` + * call, and subsequent calls add additional key/value pairs to the original map. See {@link + * EventListParams#extraParams} for the field documentation. + */ + public Builder putExtraParam(String key, Object value) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.put(key, value); + return this; + } + + /** + * Add all map key/value pairs to `extraParams` map. A map is initialized for the first + * `put/putAll` call, and subsequent calls add additional key/value pairs to the original map. + * See {@link EventListParams#extraParams} for the field documentation. + */ + public Builder putAllExtraParam(Map map) { + if (this.extraParams == null) { + this.extraParams = new HashMap<>(); + } + this.extraParams.putAll(map); + return this; + } + + public Builder setLimit(Integer limit) { + this.limit = limit; + return this; + } + + /** Required. Primary object ID used to retrieve related events. */ + public Builder setObjectId(String objectId) { + this.objectId = objectId; + return this; + } + + public Builder setPage(String page) { + this.page = page; + return this; + } + } +} diff --git a/src/main/java/com/stripe/service/V2Services.java b/src/main/java/com/stripe/service/V2Services.java new file mode 100644 index 00000000000..6138c347dd4 --- /dev/null +++ b/src/main/java/com/stripe/service/V2Services.java @@ -0,0 +1,19 @@ +// File generated from our OpenAPI spec +package com.stripe.service; + +import com.stripe.net.ApiService; +import com.stripe.net.StripeResponseGetter; + +public final class V2Services extends ApiService { + public V2Services(StripeResponseGetter responseGetter) { + super(responseGetter); + } + + public com.stripe.service.v2.BillingService billing() { + return new com.stripe.service.v2.BillingService(this.getResponseGetter()); + } + + public com.stripe.service.v2.CoreService core() { + return new com.stripe.service.v2.CoreService(this.getResponseGetter()); + } +} diff --git a/src/main/java/com/stripe/service/v2/BillingService.java b/src/main/java/com/stripe/service/v2/BillingService.java new file mode 100644 index 00000000000..9c33a8697d1 --- /dev/null +++ b/src/main/java/com/stripe/service/v2/BillingService.java @@ -0,0 +1,27 @@ +// File generated from our OpenAPI spec +package com.stripe.service.v2; + +import com.stripe.net.ApiService; +import com.stripe.net.StripeResponseGetter; + +public final class BillingService extends ApiService { + public BillingService(StripeResponseGetter responseGetter) { + super(responseGetter); + } + + public com.stripe.service.v2.billing.MeterEventAdjustmentService meterEventAdjustments() { + return new com.stripe.service.v2.billing.MeterEventAdjustmentService(this.getResponseGetter()); + } + + public com.stripe.service.v2.billing.MeterEventSessionService meterEventSession() { + return new com.stripe.service.v2.billing.MeterEventSessionService(this.getResponseGetter()); + } + + public com.stripe.service.v2.billing.MeterEventStreamService meterEventStream() { + return new com.stripe.service.v2.billing.MeterEventStreamService(this.getResponseGetter()); + } + + public com.stripe.service.v2.billing.MeterEventService meterEvents() { + return new com.stripe.service.v2.billing.MeterEventService(this.getResponseGetter()); + } +} diff --git a/src/main/java/com/stripe/service/v2/CoreService.java b/src/main/java/com/stripe/service/v2/CoreService.java new file mode 100644 index 00000000000..313e025c51c --- /dev/null +++ b/src/main/java/com/stripe/service/v2/CoreService.java @@ -0,0 +1,15 @@ +// File generated from our OpenAPI spec +package com.stripe.service.v2; + +import com.stripe.net.ApiService; +import com.stripe.net.StripeResponseGetter; + +public final class CoreService extends ApiService { + public CoreService(StripeResponseGetter responseGetter) { + super(responseGetter); + } + + public com.stripe.service.v2.core.EventService events() { + return new com.stripe.service.v2.core.EventService(this.getResponseGetter()); + } +} diff --git a/src/main/java/com/stripe/service/v2/billing/MeterEventAdjustmentService.java b/src/main/java/com/stripe/service/v2/billing/MeterEventAdjustmentService.java new file mode 100644 index 00000000000..af09098b89d --- /dev/null +++ b/src/main/java/com/stripe/service/v2/billing/MeterEventAdjustmentService.java @@ -0,0 +1,38 @@ +// File generated from our OpenAPI spec +package com.stripe.service.v2.billing; + +import com.stripe.exception.StripeException; +import com.stripe.model.v2.billing.MeterEventAdjustment; +import com.stripe.net.ApiRequest; +import com.stripe.net.ApiRequestParams; +import com.stripe.net.ApiResource; +import com.stripe.net.ApiService; +import com.stripe.net.BaseAddress; +import com.stripe.net.RequestOptions; +import com.stripe.net.StripeResponseGetter; +import com.stripe.param.v2.billing.MeterEventAdjustmentCreateParams; + +public final class MeterEventAdjustmentService extends ApiService { + public MeterEventAdjustmentService(StripeResponseGetter responseGetter) { + super(responseGetter); + } + + /** Creates a meter event adjustment to cancel a previously sent meter event. */ + public MeterEventAdjustment create(MeterEventAdjustmentCreateParams params) + throws StripeException { + return create(params, (RequestOptions) null); + } + /** Creates a meter event adjustment to cancel a previously sent meter event. */ + public MeterEventAdjustment create( + MeterEventAdjustmentCreateParams params, RequestOptions options) throws StripeException { + String path = "/v2/billing/meter_event_adjustments"; + ApiRequest request = + new ApiRequest( + BaseAddress.API, + ApiResource.RequestMethod.POST, + path, + ApiRequestParams.paramsToMap(params), + options); + return this.request(request, MeterEventAdjustment.class); + } +} diff --git a/src/main/java/com/stripe/service/v2/billing/MeterEventService.java b/src/main/java/com/stripe/service/v2/billing/MeterEventService.java new file mode 100644 index 00000000000..509c97d96df --- /dev/null +++ b/src/main/java/com/stripe/service/v2/billing/MeterEventService.java @@ -0,0 +1,45 @@ +// File generated from our OpenAPI spec +package com.stripe.service.v2.billing; + +import com.stripe.exception.StripeException; +import com.stripe.model.v2.billing.MeterEvent; +import com.stripe.net.ApiRequest; +import com.stripe.net.ApiRequestParams; +import com.stripe.net.ApiResource; +import com.stripe.net.ApiService; +import com.stripe.net.BaseAddress; +import com.stripe.net.RequestOptions; +import com.stripe.net.StripeResponseGetter; +import com.stripe.param.v2.billing.MeterEventCreateParams; + +public final class MeterEventService extends ApiService { + public MeterEventService(StripeResponseGetter responseGetter) { + super(responseGetter); + } + + /** + * Creates a meter event. Events are validated synchronously, but are processed asynchronously. + * Supports up to 1,000 events per second in livemode. For higher rate-limits, please use meter + * event streams instead. + */ + public MeterEvent create(MeterEventCreateParams params) throws StripeException { + return create(params, (RequestOptions) null); + } + /** + * Creates a meter event. Events are validated synchronously, but are processed asynchronously. + * Supports up to 1,000 events per second in livemode. For higher rate-limits, please use meter + * event streams instead. + */ + public MeterEvent create(MeterEventCreateParams params, RequestOptions options) + throws StripeException { + String path = "/v2/billing/meter_events"; + ApiRequest request = + new ApiRequest( + BaseAddress.API, + ApiResource.RequestMethod.POST, + path, + ApiRequestParams.paramsToMap(params), + options); + return this.request(request, MeterEvent.class); + } +} diff --git a/src/main/java/com/stripe/service/v2/billing/MeterEventSessionService.java b/src/main/java/com/stripe/service/v2/billing/MeterEventSessionService.java new file mode 100644 index 00000000000..31658d1ebc0 --- /dev/null +++ b/src/main/java/com/stripe/service/v2/billing/MeterEventSessionService.java @@ -0,0 +1,37 @@ +// File generated from our OpenAPI spec +package com.stripe.service.v2.billing; + +import com.stripe.exception.StripeException; +import com.stripe.model.v2.billing.MeterEventSession; +import com.stripe.net.ApiRequest; +import com.stripe.net.ApiResource; +import com.stripe.net.ApiService; +import com.stripe.net.BaseAddress; +import com.stripe.net.RequestOptions; +import com.stripe.net.StripeResponseGetter; + +public final class MeterEventSessionService extends ApiService { + public MeterEventSessionService(StripeResponseGetter responseGetter) { + super(responseGetter); + } + + /** + * Creates a meter event session to send usage on the high-throughput meter event stream. + * Authentication tokens are only valid for 15 minutes, so you will need to create a new meter + * event session when your token expires. + */ + public MeterEventSession create() throws StripeException { + return create((RequestOptions) null); + } + /** + * Creates a meter event session to send usage on the high-throughput meter event stream. + * Authentication tokens are only valid for 15 minutes, so you will need to create a new meter + * event session when your token expires. + */ + public MeterEventSession create(RequestOptions options) throws StripeException { + String path = "/v2/billing/meter_event_session"; + ApiRequest request = + new ApiRequest(BaseAddress.API, ApiResource.RequestMethod.POST, path, null, options); + return this.request(request, MeterEventSession.class); + } +} diff --git a/src/main/java/com/stripe/service/v2/billing/MeterEventStreamService.java b/src/main/java/com/stripe/service/v2/billing/MeterEventStreamService.java new file mode 100644 index 00000000000..81130f135f8 --- /dev/null +++ b/src/main/java/com/stripe/service/v2/billing/MeterEventStreamService.java @@ -0,0 +1,47 @@ +// File generated from our OpenAPI spec +package com.stripe.service.v2.billing; + +import com.stripe.exception.StripeException; +import com.stripe.exception.TemporarySessionExpiredException; +import com.stripe.net.ApiRequest; +import com.stripe.net.ApiRequestParams; +import com.stripe.net.ApiResource; +import com.stripe.net.ApiService; +import com.stripe.net.BaseAddress; +import com.stripe.net.RequestOptions; +import com.stripe.net.StripeResponseGetter; +import com.stripe.param.v2.billing.MeterEventStreamCreateParams; +import com.stripe.v2.EmptyStripeObject; + +public final class MeterEventStreamService extends ApiService { + public MeterEventStreamService(StripeResponseGetter responseGetter) { + super(responseGetter); + } + + /** + * Creates meter events. Events are processed asynchronously, including validation. Requires a + * meter event session for authentication. Supports up to 10,000 requests per second in livemode. + * For even higher rate-limits, contact sales. + */ + public void create(MeterEventStreamCreateParams params) + throws StripeException, TemporarySessionExpiredException { + create(params, (RequestOptions) null); + } + /** + * Creates meter events. Events are processed asynchronously, including validation. Requires a + * meter event session for authentication. Supports up to 10,000 requests per second in livemode. + * For even higher rate-limits, contact sales. + */ + public void create(MeterEventStreamCreateParams params, RequestOptions options) + throws StripeException, TemporarySessionExpiredException { + String path = "/v2/billing/meter_event_stream"; + ApiRequest request = + new ApiRequest( + BaseAddress.METER_EVENTS, + ApiResource.RequestMethod.POST, + path, + ApiRequestParams.paramsToMap(params), + options); + this.request(request, EmptyStripeObject.class); + } +} diff --git a/src/main/java/com/stripe/service/v2/core/EventService.java b/src/main/java/com/stripe/service/v2/core/EventService.java new file mode 100644 index 00000000000..5732a916f2f --- /dev/null +++ b/src/main/java/com/stripe/service/v2/core/EventService.java @@ -0,0 +1,50 @@ +// File generated from our OpenAPI spec +package com.stripe.service.v2.core; + +import com.google.gson.reflect.TypeToken; +import com.stripe.exception.StripeException; +import com.stripe.model.v2.Event; +import com.stripe.model.v2.StripeCollection; +import com.stripe.net.ApiRequest; +import com.stripe.net.ApiRequestParams; +import com.stripe.net.ApiResource; +import com.stripe.net.ApiService; +import com.stripe.net.BaseAddress; +import com.stripe.net.RequestOptions; +import com.stripe.net.StripeResponseGetter; +import com.stripe.param.v2.core.EventListParams; + +public final class EventService extends ApiService { + public EventService(StripeResponseGetter responseGetter) { + super(responseGetter); + } + + /** List events, going back up to 30 days. */ + public StripeCollection list(EventListParams params) throws StripeException { + return list(params, (RequestOptions) null); + } + /** List events, going back up to 30 days. */ + public StripeCollection list(EventListParams params, RequestOptions options) + throws StripeException { + String path = "/v2/core/events"; + ApiRequest request = + new ApiRequest( + BaseAddress.API, + ApiResource.RequestMethod.GET, + path, + ApiRequestParams.paramsToMap(params), + options); + return this.request(request, new TypeToken>() {}.getType()); + } + /** Retrieves the details of an event. */ + public Event retrieve(String id) throws StripeException { + return retrieve(id, (RequestOptions) null); + } + /** Retrieves the details of an event. */ + public Event retrieve(String id, RequestOptions options) throws StripeException { + String path = String.format("/v2/core/events/%s", ApiResource.urlEncodeId(id)); + ApiRequest request = + new ApiRequest(BaseAddress.API, ApiResource.RequestMethod.GET, path, null, options); + return this.request(request, Event.class); + } +} diff --git a/src/main/java/com/stripe/v2/Amount.java b/src/main/java/com/stripe/v2/Amount.java new file mode 100644 index 00000000000..b2d0a879131 --- /dev/null +++ b/src/main/java/com/stripe/v2/Amount.java @@ -0,0 +1,22 @@ +// NOT codegenned +package com.stripe.v2; + +import com.google.gson.annotations.SerializedName; +import com.stripe.model.StripeObject; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = false) +public final class Amount extends StripeObject { + public Amount(long value, String currency) { + this.value = value; + this.currency = currency; + } + + @SerializedName("value") + long value; + + @SerializedName("currency") + String currency; +} diff --git a/src/main/java/com/stripe/v2/EmptyStripeObject.java b/src/main/java/com/stripe/v2/EmptyStripeObject.java new file mode 100644 index 00000000000..19e49ad60b9 --- /dev/null +++ b/src/main/java/com/stripe/v2/EmptyStripeObject.java @@ -0,0 +1,10 @@ +package com.stripe.v2; + +import com.stripe.model.StripeObject; + +/** + * An empty entity for API methods with a void return type. We need a class to to deserialize into, + * but we can't instantiate the abstract `StripeObject` directly. This class shouldn't do anything. + * It's handwritten, not auto-generated. + */ +public final class EmptyStripeObject extends StripeObject {} diff --git a/src/test/java/com/stripe/BaseStripeTest.java b/src/test/java/com/stripe/BaseStripeTest.java index 271b20da3f6..9e65357b47e 100644 --- a/src/test/java/com/stripe/BaseStripeTest.java +++ b/src/test/java/com/stripe/BaseStripeTest.java @@ -47,6 +47,8 @@ public class BaseStripeTest { private String origClientId; private String origUploadBase; + protected static final String TEST_API_KEY = "sk_test_123"; + static { // To only stop stripe-mock process after all the test classes. // Alternative solution using @AfterClass will stop the stripe-mock after @@ -125,7 +127,8 @@ public void setUpStripeMockUsage() { httpClientSpy = Mockito.spy(new HttpURLConnectionClient()); networkSpy = Mockito.spy(new LiveStripeResponseGetter(null, httpClientSpy)); mockClient = new StripeClient(networkSpy); - ApiResource.setStripeResponseGetter(networkSpy); + + ApiResource.setGlobalResponseGetter(networkSpy); OAuth.setGlobalResponseGetter(networkSpy); } @@ -135,7 +138,7 @@ public void setUpStripeMockUsage() { */ @AfterEach public void tearDownStripeMockUsage() { - ApiResource.setStripeResponseGetter(new LiveStripeResponseGetter()); + ApiResource.setGlobalResponseGetter(new LiveStripeResponseGetter()); Stripe.overrideApiBase(this.origApiBase); Stripe.overrideUploadBase(this.origUploadBase); @@ -313,7 +316,7 @@ public static void stubRequest( * @param path request path (e.g. "/v1/charges"). Can also be an abolute URL. * @param params map containing the parameters. If null, the parameters are not checked. * @param options request options. If null, the options are not checked. - * @param clazz Class of the API resource that will be returned for the stubbed request. + * @param typeToken Class of the API resource that will be returned for the stubbed request. * @param response JSON payload of the API resource that will be returned for the stubbed request. */ public static void stubRequest( diff --git a/src/test/java/com/stripe/DocumentationTest.java b/src/test/java/com/stripe/DocumentationTest.java index ac0efba4c7d..aa1e6a35235 100644 --- a/src/test/java/com/stripe/DocumentationTest.java +++ b/src/test/java/com/stripe/DocumentationTest.java @@ -1,10 +1,8 @@ package com.stripe; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import com.google.common.base.Joiner; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -12,10 +10,7 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Calendar; -import java.util.List; -import java.util.regex.Pattern; import org.junit.jupiter.api.Test; public class DocumentationTest { @@ -28,90 +23,6 @@ private static String formatDateTime() { return result; } - @Test - public void testChangeLogContainsStaticVersion() throws IOException { - final File changelogFile = new File("CHANGELOG.md").getAbsoluteFile(); - - assertTrue( - changelogFile.exists(), - String.format( - "Expected CHANGELOG file to exist, but it doesn't. (path is %s).", - changelogFile.getAbsolutePath())); - assertTrue( - changelogFile.isFile(), - String.format( - "Expected CHANGELOG to be a file, but it isn't. (path is %s).", - changelogFile.getAbsolutePath())); - - try (final BufferedReader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(changelogFile), StandardCharsets.UTF_8))) { - final String expectedLine = formatDateTime(); - final String pattern = - String.format( - "^## %s - 20[12][0-9]-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])$", - Stripe.VERSION); - final List closeMatches = new ArrayList(); - String line; - - while ((line = reader.readLine()) != null) { - if (line.contains(Stripe.VERSION)) { - if (Pattern.matches(pattern, line)) { - return; - } - closeMatches.add(line); - } - } - - fail( - String.format( - "Expected a line of the format '%s' in the CHANGELOG, but didn't find one.%n" - + "The following lines were close, but didn't match exactly:%n'%s'", - expectedLine, Joiner.on(", ").join(closeMatches))); - } - } - - @Test - public void testReadMeContainsStripeVersionThatMatches() throws IOException { - // this will be very flaky, but we want to ensure that the readme is correct. - final File readmeFile = new File("README.md").getAbsoluteFile(); - - assertTrue( - readmeFile.exists(), - String.format( - "Expected README.md file to exist, but it doesn't. (path is %s).", - readmeFile.getAbsolutePath())); - assertTrue( - readmeFile.isFile(), - String.format( - "Expected README.md to be a file, but it doesn't. (path is %s).", - readmeFile.getAbsolutePath())); - - try (final BufferedReader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(readmeFile), StandardCharsets.UTF_8))) { - final int expectedMentionsOfVersion = 4; - // Currently three places mention the Stripe version: the latest Maven JAR hyperlink, the - // sample pom, and gradle files. - final List mentioningLines = new ArrayList(); - String line; - - while ((line = reader.readLine()) != null) { - if (line.contains(Stripe.VERSION)) { - mentioningLines.add(line); - } - } - - final String message = - String.format( - "Expected %d mentions of the stripe-java version in the Readme, but found %d:%n%s", - expectedMentionsOfVersion, - mentioningLines.size(), - Joiner.on(", ").join(mentioningLines)); - assertSame(expectedMentionsOfVersion, mentioningLines.size(), message); - } - } - @Test public void testGradlePropertiesContainsVersionThatMatches() throws IOException { // we want to ensure that the pom's version matches the static version. diff --git a/src/test/java/com/stripe/RawRequestTest.java b/src/test/java/com/stripe/RawRequestTest.java new file mode 100644 index 00000000000..78b15ca2197 --- /dev/null +++ b/src/test/java/com/stripe/RawRequestTest.java @@ -0,0 +1,315 @@ +package com.stripe; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.stripe.exception.StripeException; +import com.stripe.model.Customer; +import com.stripe.net.ApiResource.RequestMethod; +import com.stripe.net.HttpURLConnectionClient; +import com.stripe.net.LiveStripeResponseGetter; +import com.stripe.net.RawRequestOptions; +import com.stripe.net.StripeResponse; +import com.stripe.net.StripeResponseGetter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class RawRequestTest extends BaseStripeTest { + private static MockWebServer server; + private static StripeClient client; + private StripeResponseGetter responseGetter; + + @BeforeEach + void setUp() throws IOException { + server = new MockWebServer(); + server.start(); + Stripe.overrideApiBase(server.url("").toString()); + responseGetter = + Mockito.spy( + new LiveStripeResponseGetter( + StripeClient.builder() + .setApiKey(TEST_API_KEY) + .setApiBase(server.url("").toString()) + .buildOptions(), + new HttpURLConnectionClient())); + client = new StripeClient(responseGetter); + } + + @AfterEach + void tearDown() throws IOException { + server.shutdown(); + } + + @Test + public void testStandardRequestGlobal() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody( + "{\"id\": \"cus_123\",\n \"object\": \"customer\",\n \"description\": \"test customer\"}")); + + final RawRequestOptions options = RawRequestOptions.builder().setApiKey("sk_123").build(); + + final StripeResponse response = + client.rawRequest( + RequestMethod.POST, "/v1/customers", "description=test+customer", options); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + + RecordedRequest request = server.takeRequest(); + assertEquals( + "application/x-www-form-urlencoded;charset=UTF-8", request.getHeader("Content-Type")); + assertEquals(Stripe.API_VERSION, request.getHeader("Stripe-Version")); + assertEquals("description=test+customer", request.getBody().readUtf8()); + } + + @Test + public void testNullOptionsGlobal() throws StripeException, InterruptedException { + server.enqueue(new MockResponse().setBody("{}")); + final StripeResponse response = + client.rawRequest(RequestMethod.POST, "/v1/customers", "description=test+customer", null); + assertNotNull(response); + } + + @Test + public void testV2PostRequestGlobal() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody("{\"id\": \"sub_sched_123\",\n \"object\": \"subscription_schedule\"}")); + final RawRequestOptions options = RawRequestOptions.builder().setApiKey("sk_123").build(); + + final StripeResponse response = + client.rawRequest( + RequestMethod.POST, "/v2/core/event", "{\"event_id\": \"evnt_123\"}", options); + + RecordedRequest request = server.takeRequest(); + assertEquals("application/json", request.getHeader("Content-Type")); + assertEquals(Stripe.API_VERSION, request.getHeader("Stripe-Version")); + assertEquals("{\"event_id\": \"evnt_123\"}", request.getBody().readUtf8()); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + } + + @Test + public void testPreviewGetRequestGlobal() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody("{\"id\": \"sub_sched_123\",\n \"object\": \"subscription_schedule\"}")); + final RawRequestOptions options = RawRequestOptions.builder().setApiKey("sk_123").build(); + + final StripeResponse response = + client.rawRequest(RequestMethod.GET, "/v1/subscription_schedules", "", options); + + RecordedRequest request = server.takeRequest(); + assertEquals(null, request.getHeader("Content-Type")); + assertEquals(Stripe.API_VERSION, request.getHeader("Stripe-Version")); + assertEquals("", request.getBody().readUtf8()); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + } + + @Test + public void testAdditionalHeadersGlobal() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody( + "{\"id\": \"cus_123\",\n \"object\": \"customer\",\n \"description\": \"test customer\"}")); + + Map additionalHeaders = new HashMap<>(); + + additionalHeaders.put("foo", "bar"); + final RawRequestOptions options = + RawRequestOptions.builder().setAdditionalHeaders(additionalHeaders).build(); + + assertEquals(additionalHeaders, options.getAdditionalHeaders()); + + final StripeResponse response = + client.rawRequest(RequestMethod.GET, "/v1/customers", null, options); + + RecordedRequest request = server.takeRequest(); + assertEquals("bar", request.getHeader("foo")); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + } + + @Test + public void testDeserializeGlobal() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody( + "{\"id\": \"cus_123\",\n \"object\": \"customer\",\n \"description\": \"test customer\"}")); + + final RawRequestOptions options = RawRequestOptions.builder().setApiKey("sk_123").build(); + + final StripeResponse response = + client.rawRequest( + RequestMethod.POST, "/v1/customers", "description=test+customer", options); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + + Customer customer = (Customer) client.deserialize(response.body()); + assertTrue(customer.getId().startsWith("cus_")); + assertEquals("test customer", customer.getDescription()); + } + + @Test + public void testRaisesErrorWhenGetRequestAndContentIsNonNullGlobal() throws StripeException { + try { + client.rawRequest(RequestMethod.GET, "/v1/customers", "key=value!", null); + fail("Expected illegal argument exception."); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("content is not allowed for non-POST requests.")); + } + } + + @Test + public void testNullOptionsClient() throws StripeException, InterruptedException { + server.enqueue(new MockResponse().setBody("{}")); + final StripeResponse response = + client.rawRequest(RequestMethod.POST, "/v1/customers", "description=test+customer", null); + assertNotNull(response); + } + + @Test + public void testV1RequestClient() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody( + "{\"id\": \"cus_123\",\n \"object\": \"customer\",\n \"description\": \"test customer\"}")); + + final RawRequestOptions options = RawRequestOptions.builder().setApiKey("sk_123").build(); + + final StripeResponse response = + client.rawRequest( + RequestMethod.POST, "/v1/customers", "description=test+customer", options); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + + RecordedRequest request = server.takeRequest(); + assertEquals( + "application/x-www-form-urlencoded;charset=UTF-8", request.getHeader("Content-Type")); + assertEquals(Stripe.API_VERSION, request.getHeader("Stripe-Version")); + assertEquals("description=test+customer", request.getBody().readUtf8()); + } + + @Test + public void testV2PostRequestClient() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody("{\"id\": \"sub_sched_123\",\n \"object\": \"subscription_schedule\"}")); + final RawRequestOptions options = RawRequestOptions.builder().setApiKey("sk_123").build(); + + final StripeResponse response = + client.rawRequest( + RequestMethod.POST, "/v2/core/events", "{\"event_id\": \"evnt_123\"}", options); + + RecordedRequest request = server.takeRequest(); + assertEquals("application/json", request.getHeader("Content-Type")); + assertEquals(Stripe.API_VERSION, request.getHeader("Stripe-Version")); + assertEquals("{\"event_id\": \"evnt_123\"}", request.getBody().readUtf8()); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + } + + @Test + public void testPreviewGetRequestClient() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody("{\"id\": \"sub_sched_123\",\n \"object\": \"subscription_schedule\"}")); + final RawRequestOptions options = RawRequestOptions.builder().setApiKey("sk_123").build(); + + final StripeResponse response = + client.rawRequest(RequestMethod.GET, "/v1/subscription_schedules", "", options); + + RecordedRequest request = server.takeRequest(); + assertEquals(null, request.getHeader("Content-Type")); + assertEquals(Stripe.API_VERSION, request.getHeader("Stripe-Version")); + assertEquals("", request.getBody().readUtf8()); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + } + + @Test + public void testAdditionalHeadersClient() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody( + "{\"id\": \"cus_123\",\n \"object\": \"customer\",\n \"description\": \"test customer\"}")); + + Map additionalHeaders = new HashMap<>(); + + additionalHeaders.put("foo", "bar"); + final RawRequestOptions options = + RawRequestOptions.builder().setAdditionalHeaders(additionalHeaders).build(); + + assertEquals(additionalHeaders, options.getAdditionalHeaders()); + + final StripeResponse response = + client.rawRequest(RequestMethod.GET, "/v1/customers", null, options); + + RecordedRequest request = server.takeRequest(); + assertEquals("bar", request.getHeader("foo")); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + } + + @Test + public void testDeserializeClient() throws StripeException, InterruptedException { + server.enqueue( + new MockResponse() + .setBody( + "{\"id\": \"cus_123\",\n \"object\": \"customer\",\n \"description\": \"test customer\"}")); + + final RawRequestOptions options = RawRequestOptions.builder().setApiKey("sk_123").build(); + + final StripeResponse response = + client.rawRequest( + RequestMethod.POST, "/v1/customers", "description=test+customer", options); + + assertNotNull(response); + assertEquals(200, response.code()); + assertTrue(response.body().length() > 0); + + Customer customer = (Customer) client.deserialize(response.body()); + assertTrue(customer.getId().startsWith("cus_")); + assertEquals("test customer", customer.getDescription()); + assertTrue(Mockito.mockingDetails(responseGetter).getInvocations().stream().count() > 0); + } + + @Test + public void testRaisesErrorWhenGetRequestAndContentIsNonNullClient() throws StripeException { + try { + client.rawRequest(RequestMethod.GET, "/v1/customers", "key=value!", null); + fail("Expected illegal argument exception."); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("content is not allowed for non-POST requests.")); + } + } +} diff --git a/src/test/java/com/stripe/StripeClientTest.java b/src/test/java/com/stripe/StripeClientTest.java index f10573da3b4..88bc380a8e5 100644 --- a/src/test/java/com/stripe/StripeClientTest.java +++ b/src/test/java/com/stripe/StripeClientTest.java @@ -1,16 +1,90 @@ package com.stripe; +import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.stripe.exception.SignatureVerificationException; +import com.stripe.exception.StripeException; +import com.stripe.model.ThinEvent; import com.stripe.model.terminal.Reader; import com.stripe.net.*; import java.lang.reflect.Type; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.stubbing.Answer; public class StripeClientTest extends BaseStripeTest { + private Boolean originalTelemetry; + + @BeforeEach + public void setUp() { + this.originalTelemetry = Stripe.enableTelemetry; + Stripe.enableTelemetry = true; + } + + @AfterEach + public void tearDown() { + Stripe.enableTelemetry = originalTelemetry; + } + + @Test + public void testReportsStripeClientUsageTelemetry() throws StripeException { + mockClient.customers().create(); + mockClient.customers().update("cus_xyz"); + verifyStripeRequest( + (stripeRequest) -> { + assert (stripeRequest.headers().firstValue("X-Stripe-Client-Telemetry").isPresent()); + String usage = + new Gson() + .fromJson( + stripeRequest.headers().firstValue("X-Stripe-Client-Telemetry").get(), + JsonObject.class) + .get("last_request_metrics") + .getAsJsonObject() + .get("usage") + .getAsString(); + assertEquals("stripe_client", usage); + }); + } + + // TODO: https://go/j/DEVSDK-2178 + // @Test + public void testReportsRawRequestUsageTelemetry() throws StripeException { + mockClient.rawRequest( + com.stripe.net.ApiResource.RequestMethod.POST, "/v1/customers", "description=foo", null); + mockClient.rawRequest( + com.stripe.net.ApiResource.RequestMethod.POST, "/v1/customers", "description=foo", null); + verifyStripeRequest( + (stripeRequest) -> { + assert (stripeRequest.headers().firstValue("X-Stripe-Client-Telemetry").isPresent()); + JsonArray usage = + new Gson() + .fromJson( + stripeRequest.headers().firstValue("X-Stripe-Client-Telemetry").get(), + JsonObject.class) + .get("last_request_metrics") + .getAsJsonObject() + .get("usage") + .getAsJsonArray(); + assertEquals(2, usage.size()); + assertEquals("stripe_client", usage.get(0).getAsString()); + assertEquals("raw_request", usage.get(1).getAsString()); + }); + } + @Test public void testFlowsStripeResponseGetter() throws Exception { StripeResponseGetter responseGetter = Mockito.spy(new LiveStripeResponseGetter()); @@ -34,6 +108,113 @@ public void clientOptionsDefaults() { assertEquals(Stripe.LIVE_API_BASE, options.getApiBase()); assertEquals(Stripe.CONNECT_API_BASE, options.getConnectBase()); assertEquals(Stripe.UPLOAD_API_BASE, options.getFilesBase()); + assertEquals(Stripe.METER_EVENTS_API_BASE, options.getMeterEventsBase()); assertEquals(0, options.getMaxNetworkRetries()); } + + @Test + public void checksWebhookSignature() + throws InvalidKeyException, NoSuchAlgorithmException, SignatureVerificationException { + StripeClient client = new StripeClient("sk_123"); + + String payload = "{\n \"id\": \"evt_test_webhook\",\n \"object\": \"event\"\n}"; + String secret = "whsec_test_secret"; + + Map options = new HashMap<>(); + options.put("payload", payload); + options.put("secret", secret); + + String signature = WebhookTest.generateSigHeader(options); + + ThinEvent e = client.parseThinEvent(payload, signature, secret); + assertEquals(e.getId(), "evt_test_webhook"); + } + + @Test + public void failsWebhookVerification() + throws InvalidKeyException, NoSuchAlgorithmException, SignatureVerificationException { + StripeClient client = new StripeClient("sk_123"); + + String payload = "{\n \"id\": \"evt_test_webhook\",\n \"object\": \"event\"\n}"; + String secret = "whsec_test_secret"; + String signature = "bad signature"; + + assertThrows( + SignatureVerificationException.class, + () -> { + client.parseThinEvent(payload, signature, secret); + }); + } + + static final String v2PushEventWithRelatedObject = + "{\n" + + " \"id\": \"evt_234\",\n" + + " \"object\": \"event\",\n" + + " \"type\": \"financial_account.balance.opened\",\n" + + " \"created\": \"2022-02-15T00:27:45.330Z\",\n" + + " \"context\": \"context 123\",\n" + + " \"related_object\": {\n" + + " \"id\": \"fa_123\",\n" + + " \"type\": \"financial_account\",\n" + + " \"url\": \"/v2/financial_accounts/fa_123\",\n" + + " \"stripe_context\": \"acct_123\"\n" + + " }\n" + + "}"; + + static final String v2PushEventWithoutRelatedObject = + "{\n" + + " \"id\": \"evt_234\",\n" + + " \"object\": \"event\",\n" + + " \"type\": \"financial_account.balance.opened\",\n" + + " \"created\": \"2022-02-15T00:27:45.330Z\"\n" + + "}"; + + @Test + public void parsesThinEventWithoutRelatedObject() + throws InvalidKeyException, NoSuchAlgorithmException, SignatureVerificationException { + + StripeClient client = new StripeClient("sk_123"); + + String secret = "whsec_test_secret"; + + Map options = new HashMap<>(); + options.put("payload", v2PushEventWithoutRelatedObject); + options.put("secret", secret); + + String signature = WebhookTest.generateSigHeader(options); + ThinEvent baseThinEvent = + client.parseThinEvent(v2PushEventWithoutRelatedObject, signature, secret); + assertNotNull(baseThinEvent); + assertEquals("evt_234", baseThinEvent.getId()); + assertEquals("financial_account.balance.opened", baseThinEvent.getType()); + assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), baseThinEvent.created); + assertNull(baseThinEvent.context); + assertNull(baseThinEvent.relatedObject); + } + + @Test + public void parsesThinEventWithRelatedObject() + throws InvalidKeyException, NoSuchAlgorithmException, SignatureVerificationException { + + StripeClient client = new StripeClient("sk_123"); + + String secret = "whsec_test_secret"; + + Map options = new HashMap<>(); + options.put("payload", v2PushEventWithRelatedObject); + options.put("secret", secret); + + String signature = WebhookTest.generateSigHeader(options); + ThinEvent baseThinEvent = + client.parseThinEvent(v2PushEventWithRelatedObject, signature, secret); + assertNotNull(baseThinEvent); + assertEquals("evt_234", baseThinEvent.getId()); + assertEquals("financial_account.balance.opened", baseThinEvent.getType()); + assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), baseThinEvent.created); + assertEquals("context 123", baseThinEvent.context); + assertNotNull(baseThinEvent.relatedObject); + assertEquals("fa_123", baseThinEvent.relatedObject.id); + assertEquals("financial_account", baseThinEvent.relatedObject.type); + assertEquals("/v2/financial_accounts/fa_123", baseThinEvent.relatedObject.url); + } } diff --git a/src/test/java/com/stripe/functional/ErrorTest.java b/src/test/java/com/stripe/functional/ErrorTest.java index c4b77c3965f..e461a124a0e 100644 --- a/src/test/java/com/stripe/functional/ErrorTest.java +++ b/src/test/java/com/stripe/functional/ErrorTest.java @@ -1,17 +1,14 @@ package com.stripe.functional; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; import com.stripe.BaseStripeTest; import com.stripe.Stripe; -import com.stripe.exception.InvalidRequestException; -import com.stripe.exception.StripeException; +import com.stripe.exception.*; import com.stripe.exception.oauth.InvalidClientException; import com.stripe.model.Balance; -import com.stripe.net.HttpHeaders; -import com.stripe.net.OAuth; -import com.stripe.net.StripeResponse; +import com.stripe.model.StripeError; +import com.stripe.net.*; import java.io.IOException; import java.util.Collections; import lombok.Cleanup; @@ -47,6 +44,91 @@ public void testStripeError() throws StripeException, IOException, InterruptedEx assertNotNull(exception.getStripeError().getLastResponse()); } + @Test + public void testV2OutboundPaymentInsufficientFundsError() + throws StripeException, IOException, InterruptedException { + TemporarySessionExpiredException exception = null; + @Cleanup MockWebServer server = new MockWebServer(); + Mockito.doAnswer( + (Answer) + invocation -> + new StripeResponse( + 400, + HttpHeaders.of(Collections.emptyMap()), + getResourceAsString( + "/api_fixtures/error_v2_outbound_payment_insufficient_funds.json"))) + .when(httpClientSpy) + .request(Mockito.any()); + + Stripe.overrideApiBase(server.url("").toString()); + + try { + mockClient.v2().core().events().retrieve("event_123"); + } catch (TemporarySessionExpiredException e) { + exception = e; + } + + assertNotNull(exception); + assertInstanceOf(TemporarySessionExpiredException.class, exception); + assertInstanceOf(com.stripe.model.StripeError.class, exception.getStripeError()); + assertEquals("Session expired", exception.getStripeError().getMessage()); + } + + @Test + public void testV2InvalidErrorEmpty() throws StripeException, IOException, InterruptedException { + ApiException exception = null; + @Cleanup MockWebServer server = new MockWebServer(); + Mockito.doAnswer( + (Answer) + invocation -> new StripeResponse(404, HttpHeaders.of(Collections.emptyMap()), "{}")) + .when(httpClientSpy) + .request(Mockito.any()); + + Stripe.overrideApiBase(server.url("").toString()); + + try { + mockClient.v2().core().events().retrieve("event_123"); + } catch (ApiException e) { + exception = e; + } + + assertNotNull(exception); + assertInstanceOf(ApiException.class, exception); + assertNull(exception.getStripeError()); + assertNull(exception.getUserMessage()); + assertEquals("Unrecognized error type ''; code: ", exception.getMessage()); + } + + @Test + public void testV2UnknownExceptionValidError() + throws StripeException, IOException, InterruptedException { + ApiException exception = null; + @Cleanup MockWebServer server = new MockWebServer(); + Mockito.doAnswer( + (Answer) + invocation -> + new StripeResponse( + 400, + HttpHeaders.of(Collections.emptyMap()), + "{\"error\": {\"type\": \"ceci_nest_pas_une_error_type\", \"code\": \"some_error_code\", \"message\": \"good luck debugging this one\"}}")) + .when(httpClientSpy) + .request(Mockito.any()); + + Stripe.overrideApiBase(server.url("").toString()); + + try { + mockClient.v2().core().events().retrieve("event_123"); + } catch (ApiException e) { + exception = e; + } + + assertNotNull(exception); + assertInstanceOf(ApiException.class, exception); + assertInstanceOf(StripeError.class, exception.getStripeError()); + assertNull(exception.getUserMessage()); + assertEquals("good luck debugging this one; code: some_error_code", exception.getMessage()); + } + @Test public void testOAuthError() throws StripeException, IOException, InterruptedException { String oldBase = Stripe.getConnectBase(); diff --git a/src/test/java/com/stripe/functional/GeneratedExamples.java b/src/test/java/com/stripe/functional/GeneratedExamples.java index e7f95c25fa9..1901347e306 100644 --- a/src/test/java/com/stripe/functional/GeneratedExamples.java +++ b/src/test/java/com/stripe/functional/GeneratedExamples.java @@ -1995,6 +1995,24 @@ public void testCheckoutSessionsPost2Services() throws StripeException { null); } + @Test + public void testCoreEventsGetServices() throws StripeException { + stubRequest( + BaseAddress.API, + ApiResource.RequestMethod.GET, + "/v2/core/events/ll_123", + null, + null, + com.stripe.model.v2.Event.class, + "{\"context\":\"context\",\"created\":\"1970-01-12T21:42:34.472Z\",\"id\":\"obj_123\",\"livemode\":true,\"object\":\"v2.core.event\",\"reason\":{\"type\":\"request\",\"request\":{\"id\":\"obj_123\",\"idempotency_key\":\"idempotency_key\"}},\"type\":\"type\"}"); + StripeClient client = new StripeClient(networkSpy); + + com.stripe.model.v2.Event event = client.v2().core().events().retrieve("ll_123"); + assertNotNull(event); + verifyRequest( + BaseAddress.API, ApiResource.RequestMethod.GET, "/v2/core/events/ll_123", null, null); + } + @Test public void testCountrySpecsGet() throws StripeException { CountrySpecListParams params = CountrySpecListParams.builder().setLimit(3L).build(); diff --git a/src/test/java/com/stripe/functional/LiveStripeResponseGetterTest.java b/src/test/java/com/stripe/functional/LiveStripeResponseGetterTest.java index 143635f1ff6..bc89d831378 100644 --- a/src/test/java/com/stripe/functional/LiveStripeResponseGetterTest.java +++ b/src/test/java/com/stripe/functional/LiveStripeResponseGetterTest.java @@ -27,7 +27,7 @@ public class LiveStripeResponseGetterTest extends BaseStripeTest { public void testInvalidJson() throws StripeException { HttpClient spy = Mockito.spy(new HttpURLConnectionClient()); StripeResponseGetter srg = new LiveStripeResponseGetter(spy); - ApiResource.setStripeResponseGetter(srg); + ApiResource.setGlobalResponseGetter(srg); StripeResponse response = new StripeResponse(200, HttpHeaders.of(Collections.emptyMap()), "invalid JSON"); Mockito.doReturn(response).when(spy).requestWithRetries(Mockito.any()); diff --git a/src/test/java/com/stripe/functional/StripeResponseStreamTest.java b/src/test/java/com/stripe/functional/StripeResponseStreamTest.java index c71517cf87e..b42b444b401 100644 --- a/src/test/java/com/stripe/functional/StripeResponseStreamTest.java +++ b/src/test/java/com/stripe/functional/StripeResponseStreamTest.java @@ -9,7 +9,6 @@ import com.stripe.exception.InvalidRequestException; import com.stripe.exception.StripeException; import com.stripe.model.HasId; -import com.stripe.net.ApiMode; import com.stripe.net.ApiRequest; import com.stripe.net.ApiResource; import com.stripe.net.BaseAddress; @@ -47,12 +46,12 @@ public InputStream pdf() throws StripeException { String url = String.format("/v1/foobars/%s/pdf", ApiResource.urlEncodeId(this.getId())); return getResponseGetter() .requestStream( - BaseAddress.API, - ApiResource.RequestMethod.POST, - url, - (Map) null, - (RequestOptions) null, - ApiMode.V1); + new ApiRequest( + BaseAddress.FILES, + ApiResource.RequestMethod.POST, + url, + (Map) null, + (RequestOptions) null)); } } @@ -65,6 +64,7 @@ public void testStreamedResponseSuccess() server.start(); Stripe.overrideApiBase(server.url("").toString()); + Stripe.overrideUploadBase(server.url("").toString()); TestResource t = TestResource.retrieve("foo_123"); server.takeRequest(); @@ -90,6 +90,7 @@ public void testStreamedResponseFailure() server.start(); Stripe.overrideApiBase(server.url("").toString()); + Stripe.overrideUploadBase(server.url("").toString()); TestResource r = TestResource.retrieve("foo_123"); server.takeRequest(); diff --git a/src/test/java/com/stripe/functional/TimeoutTest.java b/src/test/java/com/stripe/functional/TimeoutTest.java index 07754edb846..870d5ebded7 100644 --- a/src/test/java/com/stripe/functional/TimeoutTest.java +++ b/src/test/java/com/stripe/functional/TimeoutTest.java @@ -22,7 +22,8 @@ public void testReadTimeout() throws IOException, StripeException { new ServerSocket(0, 1, Inet4Address.getByName("localhost"))) { Stripe.overrideApiBase(String.format("http://localhost:%d", serverSocket.getLocalPort())); - final RequestOptions options = RequestOptions.builder().setReadTimeout(1).build(); + final RequestOptions options = + RequestOptions.builder().setReadTimeout(1).setMaxNetworkRetries(0).build(); Throwable exception = assertThrows( diff --git a/src/test/java/com/stripe/functional/v2/StripeCollectionTest.java b/src/test/java/com/stripe/functional/v2/StripeCollectionTest.java new file mode 100644 index 00000000000..4285aac2f7e --- /dev/null +++ b/src/test/java/com/stripe/functional/v2/StripeCollectionTest.java @@ -0,0 +1,233 @@ +package com.stripe.functional.v2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import com.google.common.base.Splitter; +import com.google.gson.reflect.TypeToken; +import com.stripe.BaseStripeTest; +import com.stripe.exception.StripeException; +import com.stripe.model.v2.StripeCollection; +import com.stripe.net.ApiRequest; +import com.stripe.net.ApiResource; +import com.stripe.net.ApiResource.RequestMethod; +import com.stripe.net.ApiService; +import com.stripe.net.BaseAddress; +import com.stripe.net.HttpHeaders; +import com.stripe.net.RequestOptions; +import com.stripe.net.StripeRequest; +import com.stripe.net.StripeResponse; +import com.stripe.net.StripeResponseGetter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class StripeCollectionTest extends BaseStripeTest { + private static class PageableModel extends ApiResource { + String id; + + public String getId() { + return id; + } + } + + private static class PageableService extends ApiService { + public PageableService(StripeResponseGetter g) { + super(g); + } + + public StripeCollection list(Map params, RequestOptions options) + throws StripeException { + return this.getResponseGetter() + .request( + new ApiRequest( + BaseAddress.API, RequestMethod.GET, "/v2/pageable_models", params, options), + new TypeToken>() {}.getType()); + } + } + + List calculateJsonPages(String... pages) { + final List jsonPages = new ArrayList<>(); + int i = 0; + Integer nextPage = 0; + for (final String page : pages) { + nextPage = i++; + if (i == pages.length) { + nextPage = null; + } + List objs = new ArrayList<>(); + if (!page.equals("")) { + for (final String id : Splitter.on(',').split(page)) { + objs.add(String.format("{\"id\": \"%s\"}", id)); + } + } + + if (nextPage == null) { + jsonPages.add( + String.format("{\"data\": [%s], \"next_page_url\": null}", String.join(",", objs))); + } else { + jsonPages.add( + String.format( + "{\"data\": [%s], \"next_page_url\": \"/v2/pageable_models/page_%d\"}", + String.join(",", objs), i)); + } + } + return jsonPages; + } + + /** Sets the mock page fixtures. */ + public void setUpMockPages(String... pages) throws IOException, StripeException { + final List jsonPages = calculateJsonPages(pages); + Mockito.doAnswer( + new Answer() { + private int count = 0; + + @Override + public StripeResponse answer(InvocationOnMock invocation) { + if (count >= pages.length) { + throw new RuntimeException("Page out of bounds"); + } + + return new StripeResponse( + 200, HttpHeaders.of(Collections.emptyMap()), jsonPages.get(count++)); + } + }) + .when(httpClientSpy) + .request(Mockito.any()); + } + + @Test + public void testAutoPagingIterableEmpty() throws Exception { + setUpMockPages(""); + + final Map params = new HashMap<>(); + final RequestOptions requestOptions = RequestOptions.builder().build(); + + final StripeCollection collection = + new PageableService(BaseStripeTest.networkSpy).list(params, requestOptions); + final List models = new ArrayList<>(); + System.out.println(collection); + + for (PageableModel model : collection.autoPagingIterable()) { + models.add(model); + } + + assertEquals(0, models.size()); + verifyRequest( + BaseAddress.API, ApiResource.RequestMethod.GET, "/v2/pageable_models", params, null); + verifyNoMoreInteractions(networkSpy); + } + + @Test + public void testAutoPagingIterableSinglePage() throws Exception { + setUpMockPages("1,2,3"); + + final Map params = new HashMap<>(); + final RequestOptions requestOptions = RequestOptions.builder().build(); + + final StripeCollection collection = + new PageableService(BaseStripeTest.networkSpy).list(params, requestOptions); + final List models = new ArrayList<>(); + + for (PageableModel model : collection.autoPagingIterable()) { + models.add(model); + } + + assertEquals(3, models.size()); + assertEquals("1", models.get(0).getId()); + assertEquals("2", models.get(1).getId()); + assertEquals("3", models.get(2).getId()); + verifyRequest( + BaseAddress.API, ApiResource.RequestMethod.GET, "/v2/pageable_models", params, null); + verifyNoMoreInteractions(networkSpy); + } + + @Test + public void testAutoPagingIterableMultiplePages() throws Exception { + setUpMockPages("1,2", "3,4", "5"); + // set some arbitrary parameters so that we can verify that they're + // used for requests on the first page only + final Map page0Params = new HashMap<>(); + page0Params.put("foo", "bar"); + + final Map nextPageParams = new HashMap<>(); + + final RequestOptions ro = RequestOptions.builder().build(); + + final StripeCollection collection = + new PageableService(BaseStripeTest.networkSpy).list(page0Params, ro); + + final List models = new ArrayList<>(); + for (PageableModel model : collection.autoPagingIterable()) { + models.add(model); + } + + assertEquals(5, models.size()); + assertEquals("1", models.get(0).getId()); + assertEquals("2", models.get(1).getId()); + assertEquals("3", models.get(2).getId()); + assertEquals("4", models.get(3).getId()); + assertEquals("5", models.get(4).getId()); + + verifyRequest( + BaseAddress.API, ApiResource.RequestMethod.GET, "/v2/pageable_models", page0Params, null); + verifyRequest( + BaseAddress.API, + ApiResource.RequestMethod.GET, + "/v2/pageable_models/page_1", + nextPageParams, + null); + verifyRequest( + BaseAddress.API, + ApiResource.RequestMethod.GET, + "/v2/pageable_models/page_2", + nextPageParams, + null); + verifyNoMoreInteractions(networkSpy); + } + + @Test + public void testAutoPagingIterableWithRequestOptions() throws Exception { + setUpMockPages("1,2", "3,4", "5"); + + final RequestOptions requestOptions = + RequestOptions.builder().setApiKey("custom_api_key").build(); + + final StripeCollection collection = + new PageableService(BaseStripeTest.networkSpy).list(null, null); + + final List models = new ArrayList<>(); + for (PageableModel model : collection.autoPagingIterable(requestOptions)) { + models.add(model); + } + + // no params needed or wanted with the nextPageUrl + final Map nextPageParams = new HashMap<>(); + + assertEquals(5, models.size()); + + verifyRequest( + BaseAddress.API, ApiResource.RequestMethod.GET, "/v2/pageable_models", null, null); + verifyRequest( + BaseAddress.API, + ApiResource.RequestMethod.GET, + "/v2/pageable_models/page_1", + nextPageParams, + requestOptions); + verifyRequest( + BaseAddress.API, + ApiResource.RequestMethod.GET, + "/v2/pageable_models/page_2", + nextPageParams, + requestOptions); + + verifyNoMoreInteractions(networkSpy); + } +} diff --git a/src/test/java/com/stripe/model/InstantDeserializerTest.java b/src/test/java/com/stripe/model/InstantDeserializerTest.java new file mode 100644 index 00000000000..db179fafab1 --- /dev/null +++ b/src/test/java/com/stripe/model/InstantDeserializerTest.java @@ -0,0 +1,34 @@ +package com.stripe.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.stripe.BaseStripeTest; +import com.stripe.net.ApiResource; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +public class InstantDeserializerTest extends BaseStripeTest { + + private static Gson gson = ApiResource.GSON; + + @Test + public void deserializeNull() { + final String json = gson.toJson(null); + // Gson also uses TypeTokens internally to get around Type Erasure for generic types, simulate + // that here: + final Instant out = gson.fromJson(json, new TypeToken() {}.getType()); + assertNull(out); + } + + @Test + public void deserializeString() { + final String json = gson.toJson("2022-02-15T00:27:45.330Z"); + // Gson also uses TypeTokens internally to get around Type Erasure for generic types, simulate + // that here: + final Instant out = gson.fromJson(json, new TypeToken() {}.getType()); + assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), out); + } +} diff --git a/src/test/java/com/stripe/model/InstantSerializerTest.java b/src/test/java/com/stripe/model/InstantSerializerTest.java new file mode 100644 index 00000000000..77548b2c886 --- /dev/null +++ b/src/test/java/com/stripe/model/InstantSerializerTest.java @@ -0,0 +1,53 @@ +package com.stripe.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.stripe.BaseStripeTest; +import com.stripe.net.ApiResource; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +public class InstantSerializerTest extends BaseStripeTest { + + private static Gson gson = ApiResource.GSON; + + private static class TestTopLevelObject extends StripeObject { + @SuppressWarnings("unused") + Instant nested; + } + + @Test + public void serializeInstant() { + final TestTopLevelObject object = new TestTopLevelObject(); + object.nested = Instant.parse("2022-02-15T00:27:45.330Z"); + + final String expected = "{\n \"nested\": \"2022-02-15T00:27:45.330Z\"\n}"; + assertEquals(expected, object.toJson()); + } + + @Test + public void serializeNull() { + final TestTopLevelObject object = new TestTopLevelObject(); + object.nested = null; + + final String expected = "{\n \"nested\": null\n}"; + assertEquals(expected, object.toJson()); + } + + @Test + public void serializeDeserialize() { + final TestTopLevelObject object = new TestTopLevelObject(); + final Instant instant = Instant.parse("2022-02-15T00:27:45.330Z"); + object.nested = Instant.parse("2022-02-15T00:27:45.330Z"); + + final String json = object.toJson(); + + // Gson also uses TypeTokens internally to get around Type Erasure for generic types, simulate + // that here: + final TestTopLevelObject out = + gson.fromJson(json, new TypeToken() {}.getType()); + assertEquals(instant, out.nested); + } +} diff --git a/src/test/java/com/stripe/model/PagingIteratorTest.java b/src/test/java/com/stripe/model/PagingIteratorTest.java index 8935b3f1cf6..ad09d0d914f 100644 --- a/src/test/java/com/stripe/model/PagingIteratorTest.java +++ b/src/test/java/com/stripe/model/PagingIteratorTest.java @@ -136,6 +136,7 @@ void testAutoPaginationFromReferencedCollection() throws StripeException, IOExce "{\"id\": \"xyz\", \"pages\": {\"data\": [{\"id\": \"pm_121\"}, {\"id\": \"pm_122\"}], \"url\": \"/v1/pageable_models\", \"has_more\": true}}")) .when(httpClientSpy) .request(Mockito.any()); + Stripe.apiKey = null; ReferencesPageableModel model = ReferencesPageableModel.retrieve(RequestOptions.builder().setApiKey("sk_test_xyz").build()); diff --git a/src/test/java/com/stripe/model/SearchPagingIteratorTest.java b/src/test/java/com/stripe/model/SearchPagingIteratorTest.java index 38783e74bfb..f0a6afb5177 100644 --- a/src/test/java/com/stripe/model/SearchPagingIteratorTest.java +++ b/src/test/java/com/stripe/model/SearchPagingIteratorTest.java @@ -113,6 +113,7 @@ public void testAutoPagination() throws StripeException { assertEquals("pm_126", models.get(3).getId()); assertEquals("pm_127", models.get(4).getId()); + // First request made using a static method verifyRequest( BaseAddress.API, ApiResource.RequestMethod.GET, "/v1/searchable_models", page0Params, null); verifyRequest( diff --git a/src/test/java/com/stripe/model/StandardizationTest.java b/src/test/java/com/stripe/model/StandardizationTest.java new file mode 100644 index 00000000000..6e02f98ae4d --- /dev/null +++ b/src/test/java/com/stripe/model/StandardizationTest.java @@ -0,0 +1,156 @@ +package com.stripe.model; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.reflect.ClassPath; +import com.google.common.reflect.Invokable; +import com.google.common.reflect.Parameter; +import com.stripe.net.ApiRequestParams; +import com.stripe.net.ApiResource; +import com.stripe.net.RequestOptions; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Simple test to make sure stripe-java provides consistent bindings. */ +public class StandardizationTest { + @Test + public void allNonDeprecatedMethodsTakeOptions() throws IOException, NoSuchMethodException { + for (Class model : getAllModels()) { + for (Method method : model.getMethods()) { + // Skip methods not declared on the base class. + if (method.getDeclaringClass() != model) { + continue; + } + // Skip equals + if (method.getName().equals("equals")) { + continue; + } + // Skip hashCode + if (method.getName().equals("hashCode")) { + continue; + } + // Skip setters + if (method.getName().startsWith("set")) { + continue; + } + // Skip getters + if (method.getName().startsWith("get")) { + continue; + } + // Skip internal methods + if (method.getName().startsWith("_")) { + continue; + } + + Class[] parameterTypes = method.getParameterTypes(); + + // If more than one method with the same parameter types is declared in a class, and one of + // these methods has a return type that is more specific than any of the others, that method + // is returned; otherwise one of the methods is chosen arbitrarily. + Method mostSpecificMethod = model.getDeclaredMethod(method.getName(), parameterTypes); + if (!method.equals(mostSpecificMethod)) { + continue; + } + + Invokable invokable = Invokable.from(method); + // Skip private methods. + if (invokable.isPrivate()) { + continue; + } + // Skip deprecated methods - we need to keep them around, but aren't asserting their type. + if (invokable.isAnnotationPresent(Deprecated.class)) { + continue; + } + ImmutableList parameters = invokable.getParameters(); + // Skip empty parameter lists - assume the author is using default values for the + // RequestOptions + if (parameters.isEmpty()) { + continue; + } + Parameter lastParam = parameters.get(parameters.size() - 1); + Class finalParamType = lastParam.getType().getRawType(); + + // Skip methods that have exactly one param which is a map. + boolean isRequestParamType = + ApiRequestParams.class.isAssignableFrom(finalParamType) + || Map.class.equals(finalParamType); + if (isRequestParamType && parameters.size() == 1) { + continue; + } + + // Skip `public static Foo retrieve(String id) {...` helper methods + if (String.class.equals(finalParamType) + && parameters.size() == 1 + && method.getName().startsWith("retrieve")) { + continue; + } + + // Skip the `public static Card createCard(String id) {...` helper method on Customer. + if (String.class.equals(finalParamType) + && parameters.size() == 1 + && ("createCard".equals(method.getName()) + || "createBankAccount".equals(method.getName()))) { + continue; + } + + if (RequestOptions.class.isAssignableFrom(finalParamType)) { + continue; + } + + // Check if an overload with RequestOptions as the last parameter exists + if (!Arrays.stream(parameterTypes).anyMatch(p -> p.equals(RequestOptions.class))) { + Class[] overloadParameterTypes = + Arrays.copyOf(parameterTypes, parameterTypes.length + 1); + Array.set( + overloadParameterTypes, overloadParameterTypes.length - 1, RequestOptions.class); + Method overloadedMethod = + model.getDeclaredMethod(method.getName(), overloadParameterTypes); + if (overloadedMethod != null) { + continue; + } + } + + assertTrue( + RequestOptions.class.isAssignableFrom(finalParamType), + String.format( + "Methods on %ss like %s.%s should take a final " + + "parameter as a %s parameter, but got %s.%n", + ApiResource.class.getSimpleName(), + model.getSimpleName(), + method.getName(), + RequestOptions.class.getSimpleName(), + finalParamType.getCanonicalName())); + } + } + } + + private Collection> getAllModels() throws IOException { + Class chargeClass = Charge.class; + ClassPath classPath = ClassPath.from(chargeClass.getClassLoader()); + ImmutableSet topLevelClasses = + classPath.getTopLevelClasses(chargeClass.getPackage().getName()); + List> classList = Lists.newArrayListWithExpectedSize(topLevelClasses.size()); + for (ClassPath.ClassInfo classInfo : topLevelClasses) { + Class c = classInfo.load(); + // Skip things that aren't APIResources + if (!ApiResource.class.isAssignableFrom(c)) { + continue; + } + // Skip the APIResource itself + if (ApiResource.class == c) { + continue; + } + classList.add(classInfo.load()); + } + return classList; + } +} diff --git a/src/test/java/com/stripe/model/v2/EventTests.java b/src/test/java/com/stripe/model/v2/EventTests.java new file mode 100644 index 00000000000..8e06a7405e3 --- /dev/null +++ b/src/test/java/com/stripe/model/v2/EventTests.java @@ -0,0 +1,162 @@ +package com.stripe.model.v2; + +import static org.junit.jupiter.api.Assertions.*; + +import com.stripe.BaseStripeTest; +import com.stripe.events.V1BillingMeterErrorReportTriggeredEvent; +import com.stripe.exception.StripeException; +import com.stripe.model.billing.Meter; +import com.stripe.net.ApiResource; +import java.io.IOException; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class EventTests extends BaseStripeTest { + public static String v2PayloadNoData = null; + public static String v2PayloadWithData = null; + public static String v2UnknownEventPayload = null; + + @BeforeEach + public void setUpFixtures() { + v2PayloadNoData = + "{\n" + + " \"id\": \"evt_234\",\n" + + " \"object\": \"event\",\n" + + " \"type\": \"v1.billing.meter.error_report_triggered\",\n" + + " \"created\": \"2022-02-15T00:27:45.330Z\",\n" + + " \"related_object\": {\n" + + " \"id\": \"meter_123\",\n" + + " \"type\": \"billing.meter\",\n" + + " \"url\": \"/v1/billing/meters/meter_123\",\n" + + " \"stripe_context\": \"acct_123\"\n" + + " }\n" + + "}"; + v2PayloadWithData = + "{\n" + + " \"id\": \"evt_234\",\n" + + " \"object\": \"event\",\n" + + " \"type\": \"v1.billing.meter.error_report_triggered\",\n" + + " \"created\": \"2022-02-15T00:27:45.330Z\",\n" + + " \"related_object\": {\n" + + " \"id\": \"meter_123\",\n" + + " \"type\": \"billing.meter\",\n" + + " \"url\": \"/v1/billing/meters/meter_123\",\n" + + " \"stripe_context\": \"acct_123\"\n" + + " },\n" + + " \"data\": {\n" + + " \"developer_message_summary\": \"foo\"\n" + + " }\n" + + "}"; + + // note this is purposefully not correct; don't try and correct it! + v2UnknownEventPayload = + "{\n" + + " \"id\": \"evt_234\",\n" + + " \"object\": \"event\",\n" + + " \"type\": \"financial_account.features_updated\",\n" + + " \"created\": \"2022-02-15T00:27:45.330Z\",\n" + + " \"related_object\": {\n" + + " \"id\": \"meter_123\",\n" + + " \"type\": \"financial_account\",\n" + + " \"url\": \"/v2/financial_accounts/meter_123\",\n" + + " \"stripe_context\": \"acct_123\"\n" + + " },\n" + + " \"data\": {\n" + + " \"v1_event_id\": \"evt_789\",\n" + + " \"enabled_features\": [\"foo\"]\n" + + " }\n" + + "}"; + } + + @Test + public void parsesUnknownV2Event() { + Event event = Event.parse(v2UnknownEventPayload); + assertEquals("evt_234", event.getId()); + assertEquals("financial_account.features_updated", event.getType()); + assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), event.getCreated()); + + // no data or related object on base Event; nothing to check here. + } + + @Test + public void parsesV2Event() { + V1BillingMeterErrorReportTriggeredEvent event = + (V1BillingMeterErrorReportTriggeredEvent) Event.parse(v2PayloadNoData); + assertEquals("evt_234", event.getId()); + assertEquals("v1.billing.meter.error_report_triggered", event.getType()); + assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), event.getCreated()); + + assertEquals("meter_123", event.getRelatedObject().getId()); + assertEquals("billing.meter", event.getRelatedObject().getType()); + assertEquals("/v1/billing/meters/meter_123", event.getRelatedObject().getUrl()); + } + + @Test + public void parsesV2EventAndDeserializesEventData() throws StripeException { + V1BillingMeterErrorReportTriggeredEvent event = + (V1BillingMeterErrorReportTriggeredEvent) Event.parse(v2PayloadWithData); + event.setResponseGetter(networkSpy); + stubRequest( + ApiResource.RequestMethod.GET, + String.format("/v2/core/events/%s", event.getId()), + null, + Event.class, + v2PayloadWithData); + + V1BillingMeterErrorReportTriggeredEvent.EventData data = event.getData(); + + assertEquals("foo", data.getDeveloperMessageSummary()); + } + + @Test + public void retrieveObjectFetchesAndDeserializesObject() throws StripeException, IOException { + V1BillingMeterErrorReportTriggeredEvent event = + (V1BillingMeterErrorReportTriggeredEvent) Event.parse(v2PayloadNoData); + event.setResponseGetter(networkSpy); + stubRequest( + ApiResource.RequestMethod.GET, + "/v1/billing/meters/meter_123", + null, + Meter.class, + getResourceAsString("/api_fixtures/billing_meter.json")); + + assertEquals("/v1/billing/meters/meter_123", event.getRelatedObject().url); + assertEquals("meter_123", event.getRelatedObject().id); + assertEquals("billing.meter", event.getRelatedObject().type); + + Meter meter = event.fetchRelatedObject(); + + assertEquals("meter_123", meter.getId()); + assertEquals("billing.meter", meter.getObject()); + assertEquals(1727303036, meter.getCreated()); + assertEquals("e1", meter.getCustomerMapping().getEventPayloadKey()); + assertEquals("by_id", meter.getCustomerMapping().getType()); + assertEquals("sum", meter.getDefaultAggregation().getFormula()); + assertEquals("API Requests", meter.getDisplayName()); + assertEquals("API Request Made", meter.getEventName()); + assertEquals("day", meter.getEventTimeWindow()); + assertFalse(meter.getLivemode()); + assertEquals("active", meter.getStatus()); + assertNull(meter.getStatusTransitions().getDeactivatedAt()); + assertEquals(1727303036, meter.getUpdated()); + } + + // FIXME (jar) this should no longer be possible; confirm this and remove before merge + // @Test + // public void retrieveObjectFetchesAndDeserializesUnknownObject() + // throws StripeException, IOException { + // FinancialAccountBalanceOpenedEvent event = + // (FinancialAccountBalanceOpenedEvent) + // Event.parse(v2PayloadNoData.replace("\"type\": \"card\"", "\"type\": \"cardio\"")); + // event.setResponseGetter(networkSpy); + // stubRequest( + // ApiResource.RequestMethod.GET, + // "/v2/financial_accounts/meter_123", + // null, + // StripeRawJsonObject.class, + // getResourceAsString("/api_fixtures/card.json")); + + // assertInstanceOf(StripeRawJsonObject.class, event.fetchObject()); + // } +} diff --git a/src/test/java/com/stripe/net/ApiRequestParamsConverterTest.java b/src/test/java/com/stripe/net/ApiRequestParamsConverterTest.java index 22805238097..88a9f532212 100644 --- a/src/test/java/com/stripe/net/ApiRequestParamsConverterTest.java +++ b/src/test/java/com/stripe/net/ApiRequestParamsConverterTest.java @@ -7,10 +7,12 @@ import com.google.gson.annotations.SerializedName; import com.stripe.param.common.EmptyParam; +import java.time.Instant; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import junit.framework.TestCase; import org.junit.jupiter.api.Test; public class ApiRequestParamsConverterTest { @@ -115,6 +117,11 @@ private static class IllegalModelHasWrongExtraParamsType extends ApiRequestParam String extraParams; } + private static class HasInstantParam extends ApiRequestParams { + @SerializedName("instant_param") + public Instant instantParam; + } + @Test public void testHasExtraParams() { ModelHasExtraParams params = new ModelHasExtraParams(ParamCode.ENUM_FOO); @@ -281,6 +288,18 @@ public void testObjectMaps() { assertEquals(objBar.get("hello"), "world"); } + @Test + public void testToMapWithInstantParams() { + HasInstantParam params = new HasInstantParam(); + params.instantParam = Instant.ofEpochSecond(987654321); + Map paramMap = toMap(params); + TestCase.assertEquals(1, paramMap.size()); + + // primitive boolean is default false and is converted into map param accordingly + TestCase.assertTrue(paramMap.containsKey("instant_param")); + TestCase.assertEquals("2001-04-19T04:25:21Z", paramMap.get("instant_param")); + } + private Map toMap(ApiRequestParams params) { return converter.convert(params); } diff --git a/src/test/java/com/stripe/net/ApiRequestParamsTest.java b/src/test/java/com/stripe/net/ApiRequestParamsTest.java index fb903d2fd76..ecdd2ad967f 100644 --- a/src/test/java/com/stripe/net/ApiRequestParamsTest.java +++ b/src/test/java/com/stripe/net/ApiRequestParamsTest.java @@ -9,7 +9,7 @@ import com.stripe.param.common.EmptyParam; import java.util.Map; import lombok.Setter; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class ApiRequestParamsTest { enum ParamCode implements ApiRequestParams.EnumParam { diff --git a/src/test/java/com/stripe/net/ClientOptionsTest.java b/src/test/java/com/stripe/net/ClientOptionsTest.java index 673ed9045b9..139b8d39974 100644 --- a/src/test/java/com/stripe/net/ClientOptionsTest.java +++ b/src/test/java/com/stripe/net/ClientOptionsTest.java @@ -21,6 +21,7 @@ public void GlobalClientOptionsReflectsGlobalConfiguration() { String origApiBase = Stripe.getApiBase(); String origUploadBase = Stripe.getUploadBase(); String origConnectBase = Stripe.getConnectBase(); + String origMeterEventsBase = Stripe.getMeterEventsBase(); GlobalStripeResponseGetterOptions global = GlobalStripeResponseGetterOptions.INSTANCE; @@ -39,6 +40,7 @@ public void GlobalClientOptionsReflectsGlobalConfiguration() { Stripe.overrideApiBase("http://api.base"); Stripe.overrideConnectBase("http://connect.base"); Stripe.overrideUploadBase("http://upload.base"); + Stripe.overrideMeterEventsBase("http://meter-events.base"); assertEquals(1, global.getConnectTimeout()); assertEquals(1, global.getMaxNetworkRetries()); @@ -49,6 +51,7 @@ public void GlobalClientOptionsReflectsGlobalConfiguration() { assertEquals("http://api.base", global.getApiBase()); assertEquals("http://connect.base", global.getConnectBase()); assertEquals("http://upload.base", global.getFilesBase()); + assertEquals("http://meter-events.base", global.getMeterEventsBase()); } finally { Stripe.apiKey = origApiKey; Stripe.setConnectTimeout(origConnectTimeout); @@ -60,6 +63,7 @@ public void GlobalClientOptionsReflectsGlobalConfiguration() { Stripe.overrideApiBase(origApiBase); Stripe.overrideConnectBase(origConnectBase); Stripe.overrideUploadBase(origUploadBase); + Stripe.overrideMeterEventsBase(origMeterEventsBase); } } } diff --git a/src/test/java/com/stripe/net/FormEncoderTest.java b/src/test/java/com/stripe/net/FormEncoderTest.java index bad157d52e1..e3e0289628c 100644 --- a/src/test/java/com/stripe/net/FormEncoderTest.java +++ b/src/test/java/com/stripe/net/FormEncoderTest.java @@ -26,7 +26,10 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.RequiredArgsConstructor; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; @@ -127,9 +130,12 @@ public void testCreateQueryString() { org.junit.Assume.assumeTrue(!System.getProperty("java.version").startsWith("10.")); @Data + @RequiredArgsConstructor + @AllArgsConstructor class TestCase { private final Map data; private final String want; + @Nullable private Boolean arrayAsRepeated = false; } List testCases = @@ -370,6 +376,18 @@ class TestCase { "array", new Object[] {new String[] {"foo", "bar"}, new int[] {1, 2, 3}}), "array[0][0]=foo&array[0][1]=bar&array[1][0]=1&array[1][1]=2&array[1][2]=3")); + // Array (arrayAsRepeated) + add(new TestCase(Collections.singletonMap("array", new String[] {}), "", true)); + add( + new TestCase( + Collections.singletonMap("array", new String[] {"1", "2", "3"}), + "array=1&array=2&array=3", + true)); + add( + new TestCase( + Collections.singletonMap("array", new Object[] {123, "foo"}), + "array=123&array=foo", + true)); // Collection add( new TestCase( @@ -411,7 +429,9 @@ class TestCase { }; for (TestCase testCase : testCases) { - assertEquals(testCase.getWant(), FormEncoder.createQueryString(testCase.getData())); + assertEquals( + testCase.getWant(), + FormEncoder.createQueryString(testCase.getData(), testCase.getArrayAsRepeated())); } } @@ -461,7 +481,7 @@ class TestCase { }; for (TestCase testCase : testCases) { - assertEquals(testCase.getWant(), FormEncoder.flattenParams(testCase.getData())); + assertEquals(testCase.getWant(), FormEncoder.flattenParams(testCase.getData(), false)); } } diff --git a/src/test/java/com/stripe/net/HttpClientTest.java b/src/test/java/com/stripe/net/HttpClientTest.java index 144bd9b0a54..be97783af65 100644 --- a/src/test/java/com/stripe/net/HttpClientTest.java +++ b/src/test/java/com/stripe/net/HttpClientTest.java @@ -34,11 +34,12 @@ public void setUpFixtures() throws StripeException { this.client.networkRetriesSleep = false; this.request = - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.GET, "http://example.com/get", null, - RequestOptions.builder().setApiKey("sk_test_123").setMaxNetworkRetries(2).build()); + RequestOptions.builder().setApiKey("sk_test_123").setMaxNetworkRetries(2).build(), + ApiMode.V1); } @Test diff --git a/src/test/java/com/stripe/net/HttpContentTest.java b/src/test/java/com/stripe/net/HttpContentTest.java index dd9ee043d65..3bbada57aaf 100644 --- a/src/test/java/com/stripe/net/HttpContentTest.java +++ b/src/test/java/com/stripe/net/HttpContentTest.java @@ -8,16 +8,24 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.junit.jupiter.api.Test; public class HttpContentTest extends BaseStripeTest { @Test public void testBuildFormURLEncodedContentNull() throws IOException { + String stringContent = null; + Collection> nameValueCollectionContent = null; assertThrows( NullPointerException.class, () -> { - HttpContent.buildFormURLEncodedContent(null); + HttpContent.buildFormURLEncodedContent(stringContent); + }); + assertThrows( + NullPointerException.class, + () -> { + HttpContent.buildFormURLEncodedContent(nameValueCollectionContent); }); } diff --git a/src/test/java/com/stripe/net/JsonEncoderTest.java b/src/test/java/com/stripe/net/JsonEncoderTest.java new file mode 100644 index 00000000000..79cc8879762 --- /dev/null +++ b/src/test/java/com/stripe/net/JsonEncoderTest.java @@ -0,0 +1,67 @@ +package com.stripe.net; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.google.gson.annotations.SerializedName; +import com.stripe.BaseStripeTest; +import com.stripe.param.common.EmptyParam; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +public class JsonEncoderTest extends BaseStripeTest { + enum TestEnum { + @SerializedName("foo") + FOO, + + @SerializedName("bar") + BAR, + } + + static class TestParams extends ApiRequestParams { + @SerializedName("name") + public Object name; + + @SerializedName("nested") + public NestedParams nested; + + @SerializedName("enum") + public TestEnum eenum; + } + + static class NestedParams { + @SerializedName("email") + public Object email; + } + + @Test + public void testCreateJsonContent() throws IOException { + TestParams params = new TestParams(); + params.name = "name"; + params.nested = new NestedParams(); + params.nested.email = "a@example.com"; + params.eenum = TestEnum.FOO; + + HttpContent content = JsonEncoder.createHttpContent(ApiRequestParams.paramsToMap(params)); + + assertNotNull(content); + assertEquals("application/json", content.contentType()); + assertEquals( + "{\"name\":\"name\",\"nested\":{\"email\":\"a@example.com\"},\"enum\":\"foo\"}", + content.stringContent()); + } + + @Test + public void testCreateJsonContentEmptyParam() throws IOException { + TestParams params = new TestParams(); + params.name = EmptyParam.EMPTY; + params.nested = new NestedParams(); + params.nested.email = EmptyParam.EMPTY; + + HttpContent content = JsonEncoder.createHttpContent(ApiRequestParams.paramsToMap(params)); + + assertNotNull(content); + assertEquals("application/json", content.contentType()); + assertEquals("{\"name\":null,\"nested\":{\"email\":null}}", content.stringContent()); + } +} diff --git a/src/test/java/com/stripe/net/RequestOptionsTest.java b/src/test/java/com/stripe/net/RequestOptionsTest.java index 1e4a1a852b6..156ee92a1e3 100644 --- a/src/test/java/com/stripe/net/RequestOptionsTest.java +++ b/src/test/java/com/stripe/net/RequestOptionsTest.java @@ -132,13 +132,14 @@ public void mergeOverwritesClientOptions() { StripeResponseGetterOptions clientOptions = TestStripeResponseGetterOptions.builder() - .setApiKey("key1") + .setAuthenticator(new BearerTokenAuthenticator("key1")) .setConnectTimeout(1) .setMaxNetworkRetries(2) .setReadTimeout(3) .setClientId("1") .setConnectionProxy(clientProxy) .setProxyCredential(clientProxyCred) + .setStripeContext("globalContext") .build(); RequestOptions requestOptions = @@ -152,6 +153,7 @@ public void mergeOverwritesClientOptions() { .setProxyCredential(requestProxyCred) .setIdempotencyKey("3") .setStripeAccount("4") + .setStripeContext("5") .build(); RequestOptions merged = RequestOptions.merge(clientOptions, requestOptions); @@ -164,6 +166,7 @@ public void mergeOverwritesClientOptions() { assertEquals(requestProxyCred, merged.getProxyCredential()); assertEquals("3", merged.getIdempotencyKey()); assertEquals("4", merged.getStripeAccount()); + assertEquals("5", merged.getStripeContext()); } @Test @@ -174,13 +177,14 @@ public void mergeFallsBackToClientOptions() { StripeResponseGetterOptions clientOptions = TestStripeResponseGetterOptions.builder() - .setApiKey("key1") + .setAuthenticator(new BearerTokenAuthenticator("key1")) .setConnectTimeout(1) .setMaxNetworkRetries(1) .setReadTimeout(1) .setClientId("1") .setConnectionProxy(clientProxy) .setProxyCredential(clientProxyCred) + .setStripeContext("global context") .build(); RequestOptions requestOptions = RequestOptions.builder().build(); @@ -195,6 +199,7 @@ public void mergeFallsBackToClientOptions() { assertEquals(clientProxyCred, merged.getProxyCredential()); assertEquals(null, merged.getIdempotencyKey()); assertEquals(null, merged.getStripeAccount()); + assertEquals("global context", merged.getStripeContext()); } @Test diff --git a/src/test/java/com/stripe/net/RequestSigningAuthenticatorTest.java b/src/test/java/com/stripe/net/RequestSigningAuthenticatorTest.java new file mode 100644 index 00000000000..992cdb8b354 --- /dev/null +++ b/src/test/java/com/stripe/net/RequestSigningAuthenticatorTest.java @@ -0,0 +1,128 @@ +package com.stripe.net; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.common.collect.ImmutableMap; +import com.stripe.BaseStripeTest; +import com.stripe.exception.StripeException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +public class RequestSigningAuthenticatorTest extends BaseStripeTest { + Function testCurrentTimeGetter = + (l) -> { + return new RequestSigningAuthenticator.CurrentTimeInSecondsGetter() { + @Override + public Long getCurrentTimeInSeconds() { + return l; + } + }; + }; + + @Test + public void appliesSignatureToRequest() throws StripeException { + List signatureBases = new ArrayList<>(); + + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.POST, + "http://example.com/get", + ImmutableMap.of("string", "String!"), + RequestOptions.builder() + .setAuthenticator( + new RequestSigningAuthenticator("keyid") { + @Override + public byte[] sign(byte[] signatureBase) throws GeneralSecurityException { + signatureBases.add(signatureBase); + return new byte[] {1, 2, 3, 4, 5}; + } + }.withCurrentTimeInSecondsGetter(testCurrentTimeGetter.apply(123456789L))) + .build(), + ApiMode.V2); + + assertEquals( + "\"content-type\": application/json\n" + + "\"content-digest\": sha-256=:HA3i38j+04ac71IzPtG1JK8o4q9sPK0fYPmJHmci5bg=:\n" + + "\"stripe-context\": \n" + + "\"stripe-account\": \n" + + "\"authorization\": STRIPE-V2-SIG keyid\n" + + "\"@signature-params\": (\"content-type\" \"content-digest\" \"stripe-context\" \"stripe-account\" \"authorization\");created=123456789", + new String(signatureBases.get(0), StandardCharsets.UTF_8)); + assertEquals( + "sig1=(\"content-type\" \"content-digest\" \"stripe-context\" \"stripe-account\" \"authorization\");" + + "created=123456789", + request.headers().firstValue("Signature-Input").get()); + assertEquals("sig1=:AQIDBAU=:", request.headers().firstValue("Signature").get()); + assertEquals( + "sha-256=:HA3i38j+04ac71IzPtG1JK8o4q9sPK0fYPmJHmci5bg=:", + request.headers().firstValue("Content-Digest").get()); + assertEquals("STRIPE-V2-SIG keyid", request.headers().firstValue("Authorization").get()); + assertEquals("application/json", request.headers().firstValue("Content-Type").get()); + } + + @Test + public void appliesSignatureToGetRequest() throws StripeException { + List signatureBases = new ArrayList<>(); + + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.GET, + "http://example.com/get", + null, + RequestOptions.builder() + .setAuthenticator( + new RequestSigningAuthenticator("keyid") { + @Override + public byte[] sign(byte[] signatureBase) throws GeneralSecurityException { + signatureBases.add(signatureBase); + return new byte[] {1, 2, 3, 4, 5}; + } + }.withCurrentTimeInSecondsGetter(testCurrentTimeGetter.apply(123456789L))) + .build(), + ApiMode.V2); + + assertEquals( + "\"stripe-context\": \n" + + "\"stripe-account\": \n" + + "\"authorization\": STRIPE-V2-SIG keyid\n" + + "\"@signature-params\": (\"stripe-context\" \"stripe-account\" \"authorization\");created=123456789", + new String(signatureBases.get(0), StandardCharsets.UTF_8)); + assertEquals( + "sig1=(\"stripe-context\" \"stripe-account\" \"authorization\");" + "created=123456789", + request.headers().firstValue("Signature-Input").get()); + assertEquals("sig1=:AQIDBAU=:", request.headers().firstValue("Signature").get()); + assertEquals(false, request.headers().firstValue("Content-Digest").isPresent()); + assertEquals("STRIPE-V2-SIG keyid", request.headers().firstValue("Authorization").get()); + } + + @Test + public void wrapsSecurityException() { + StripeException exception = + assertThrows( + StripeException.class, + () -> + StripeRequest.create( + ApiResource.RequestMethod.POST, + "http://example.com/get", + ImmutableMap.of("string", "String!"), + RequestOptions.builder() + .setAuthenticator( + new RequestSigningAuthenticator("keyid") { + @Override + public byte[] sign(byte[] signatureBase) + throws GeneralSecurityException { + throw new GeneralSecurityException("something bad happened"); + } + }) + .build(), + ApiMode.V2)); + + assertEquals("Error calculating request signature.", exception.getMessage()); + assertEquals("something bad happened", exception.getCause().getMessage()); + } +} diff --git a/src/test/java/com/stripe/net/StripeRequestTest.java b/src/test/java/com/stripe/net/StripeRequestTest.java index 4b5c00b9b76..ace1f86f158 100644 --- a/src/test/java/com/stripe/net/StripeRequestTest.java +++ b/src/test/java/com/stripe/net/StripeRequestTest.java @@ -9,6 +9,7 @@ import com.stripe.exception.AuthenticationException; import com.stripe.exception.StripeException; import com.stripe.net.RequestOptions.RequestOptionsBuilder; +import com.stripe.param.common.EmptyParam; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; @@ -35,11 +36,12 @@ static class NestedParams { @Test public void testCtorGetRequest() throws StripeException { StripeRequest request = - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.GET, "http://example.com/get", ImmutableMap.of("string", "String!"), - options); + options, + ApiMode.V1); assertEquals(ApiResource.RequestMethod.GET, request.method()); assertEquals("http://example.com/get?string=String%21", request.url().toString()); @@ -54,11 +56,12 @@ public void testCtorGetRequest() throws StripeException { @Test public void testCtorGetRequestWithQueryString() throws StripeException { StripeRequest request = - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.GET, "http://example.com/get?customer=cus_xxx", ImmutableMap.of("string", "String!"), - options); + options, + ApiMode.V1); assertEquals(ApiResource.RequestMethod.GET, request.method()); assertEquals( @@ -74,11 +77,12 @@ public void testCtorGetRequestWithQueryString() throws StripeException { @Test public void testCtorPostRequest() throws StripeException { StripeRequest request = - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.POST, "http://example.com/post", ImmutableMap.of("string", "String!"), - options); + options, + ApiMode.V1); assertEquals(ApiResource.RequestMethod.POST, request.method()); assertEquals("http://example.com/post", request.url().toString()); @@ -96,11 +100,12 @@ public void testCtorPostRequest() throws StripeException { @Test public void testCtorDeleteRequest() throws StripeException { StripeRequest request = - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.DELETE, "http://example.com/get", ImmutableMap.of("string", "String!"), - options); + options, + ApiMode.V1); assertEquals(ApiResource.RequestMethod.DELETE, request.method()); assertEquals("http://example.com/get?string=String%21", request.url().toString()); @@ -112,6 +117,47 @@ public void testCtorDeleteRequest() throws StripeException { assertNull(request.content()); } + @Test + public void testCtorV2PostRequest() throws StripeException { + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.POST, + "http://example.com/post", + ImmutableMap.of("string", "String!"), + options, + ApiMode.V2); + + assertEquals(ApiResource.RequestMethod.POST, request.method()); + assertEquals("http://example.com/post", request.url().toString()); + assertEquals("Bearer sk_test_123", request.headers().firstValue("Authorization").orElse(null)); + assertTrue(request.headers().firstValue("Stripe-Version").isPresent()); + assertTrue(request.headers().firstValue("Idempotency-Key").isPresent()); + assertFalse(request.headers().firstValue("Stripe-Account").isPresent()); + assertNotNull(request.content()); + assertEquals( + "{\"string\":\"String!\"}", + new String(request.content().byteArrayContent(), StandardCharsets.UTF_8)); + } + + @Test + public void testCtorV2DeleteRequest() throws StripeException { + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.DELETE, + "http://example.com/get", + ImmutableMap.of("string", "String!"), + options, + ApiMode.V2); + + assertEquals(ApiResource.RequestMethod.DELETE, request.method()); + assertEquals("http://example.com/get?string=String%21", request.url().toString()); + assertEquals("Bearer sk_test_123", request.headers().firstValue("Authorization").orElse(null)); + assertTrue(request.headers().firstValue("Stripe-Version").isPresent()); + assertTrue(request.headers().firstValue("Idempotency-Key").isPresent()); + assertFalse(request.headers().firstValue("Stripe-Account").isPresent()); + assertNull(request.content()); + } + @Test public void testCtorRequestOptions() throws StripeException { RequestOptions options = @@ -123,7 +169,8 @@ public void testCtorRequestOptions() throws StripeException { "2012-12-21") .build(); StripeRequest request = - new StripeRequest(ApiResource.RequestMethod.GET, "http://example.com/get", null, options); + StripeRequest.create( + ApiResource.RequestMethod.GET, "http://example.com/get", null, options, ApiMode.V1); assertEquals(ApiResource.RequestMethod.GET, request.method()); assertEquals("http://example.com/get", request.url().toString()); @@ -143,11 +190,12 @@ public void testCtorThrowsOnNullApiKey() throws StripeException { assertThrows( AuthenticationException.class, () -> { - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.GET, "http://example.com/get", null, - RequestOptions.builder().build()); + RequestOptions.builder().setApiKey(null).build(), + ApiMode.V1); }); assertTrue(e.getMessage().contains("No API key provided.")); } @@ -158,46 +206,41 @@ public void testCtorThrowsOnEmptyApiKey() throws StripeException { assertThrows( AuthenticationException.class, () -> { - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.GET, "http://example.com/get", null, - RequestOptions.builder().setApiKey("").build()); + RequestOptions.builder().setApiKey("").build(), + ApiMode.V1); }); assertTrue(e.getMessage().contains("Your API key is invalid, as it is an empty string.")); } @Test public void testCtorThrowsOnApiKeyContainingWhitespace() throws StripeException { - String origApiKey = Stripe.apiKey; - - try { - Stripe.apiKey = "sk_test_123\n"; - - AuthenticationException e = - assertThrows( - AuthenticationException.class, - () -> { - new StripeRequest( - ApiResource.RequestMethod.GET, - "http://example.com/get", - null, - RequestOptions.builder().setApiKey("sk_test _123\n").build()); - }); - assertTrue(e.getMessage().contains("Your API key is invalid, as it contains whitespace.")); - } finally { - Stripe.apiKey = origApiKey; - } + AuthenticationException e = + assertThrows( + AuthenticationException.class, + () -> { + StripeRequest.create( + ApiResource.RequestMethod.GET, + "http://example.com/get", + null, + RequestOptions.builder().setApiKey("sk_test _123\n").build(), + ApiMode.V1); + }); + assertTrue(e.getMessage().contains("Your API key is invalid, as it contains whitespace.")); } @Test public void testWithAdditionalHeader() throws StripeException { StripeRequest request = - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.GET, "http://example.com/get", ImmutableMap.of("string", "String!"), - options); + options, + ApiMode.V1); StripeRequest updatedRequest = request.withAdditionalHeader("New-Header", "bar"); assertTrue(updatedRequest.headers().firstValue("New-Header").isPresent()); assertEquals("bar", updatedRequest.headers().firstValue("New-Header").get()); @@ -206,11 +249,13 @@ public void testWithAdditionalHeader() throws StripeException { @Test public void testBuildContentIsNullWhenRequestIsGet() throws StripeException { StripeRequest request = - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.GET, "http://example.com/get", ImmutableMap.of("key", "value!"), - options); + options, + ApiMode.V1); + assertNull(request.content()); } @@ -218,15 +263,94 @@ public void testBuildContentIsNullWhenRequestIsGet() throws StripeException { public void testBuildContentHasFormEncodedContentWhenRequestIsPostAndApiVersionV1() throws StripeException { StripeRequest request = - new StripeRequest( + StripeRequest.create( ApiResource.RequestMethod.POST, "http://example.com/post", ImmutableMap.of("key", "value!"), - options); + options, + ApiMode.V1); + assertInstanceOf(HttpContent.class, request.content()); assertEquals( "application/x-www-form-urlencoded;charset=UTF-8", request.content().contentType()); assertArrayEquals( "key=value%21".getBytes(StandardCharsets.UTF_8), request.content().byteArrayContent()); } + + @Test + public void testBuildHeadersHasStripeContext() throws StripeException { + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.POST, + "http://example.com/post", + null, + RequestOptions.builder().setStripeContext("ctx").setApiKey("123").build(), + ApiMode.V2); + + assertEquals("ctx", request.headers().firstValue("Stripe-Context").get()); + } + + @Test + public void testBuildHeadersIgnoresNullAccount() throws StripeException { + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.POST, + "http://example.com/post", + null, + RequestOptions.builder().setStripeAccount(null).setApiKey("123").build(), + ApiMode.V2); + + assertFalse(request.headers().map().containsKey("Stripe-Account")); + } + + @Test + public void testBuildHeadersThrowsWhenContextPassedIntoV1Request() { + assertThrows( + UnsupportedOperationException.class, + () -> + StripeRequest.create( + ApiResource.RequestMethod.POST, + "http://example.com/post", + null, + RequestOptions.builder().setStripeContext("ctx").build(), + ApiMode.V1)); + } + + @Test + public void testBuildContentHasJsonContentWhenRequestIsPostAndApiVersionV2() + throws StripeException { + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.POST, + "http://example.com/post", + ImmutableMap.of("key", "value!"), + options, + ApiMode.V2); + + assertInstanceOf(HttpContent.class, request.content()); + assertEquals("application/json", request.content().contentType()); + assertEquals("application/json", request.headers().firstValue("Content-Type").get()); + + assertArrayEquals( + "{\"key\":\"value!\"}".getBytes(StandardCharsets.UTF_8), + request.content().byteArrayContent()); + } + + @Test + public void testBuildContentEncodesEmptyParamAsNullForV2JsonRequest() throws StripeException { + TestParams params = new TestParams(); + params.name = EmptyParam.EMPTY; + params.nested = new NestedParams(); + params.nested.email = EmptyParam.EMPTY; + + StripeRequest request = + StripeRequest.create( + ApiResource.RequestMethod.POST, + "http://example.com/post", + ApiRequestParams.paramsToMap(params), + options, + ApiMode.V2); + + assertEquals("{\"name\":null,\"nested\":{\"email\":null}}", request.content().stringContent()); + } } diff --git a/src/test/java/com/stripe/net/TestStripeResponseGetterOptions.java b/src/test/java/com/stripe/net/TestStripeResponseGetterOptions.java index 55d8a1334eb..9f35d5bb9f9 100644 --- a/src/test/java/com/stripe/net/TestStripeResponseGetterOptions.java +++ b/src/test/java/com/stripe/net/TestStripeResponseGetterOptions.java @@ -10,7 +10,7 @@ public class TestStripeResponseGetterOptions extends StripeResponseGetterOptions { // When adding setting here keep them in sync with settings in RequestOptions and // in the RequestOptions.merge method - private final String apiKey; + private final Authenticator authenticator; private final String clientId; private final int connectTimeout; private final int readTimeout; @@ -20,9 +20,11 @@ public class TestStripeResponseGetterOptions extends StripeResponseGetterOptions private final String apiBase; private final String filesBase; private final String connectBase; + private final String meterEventsBase; + private final String stripeContext; public TestStripeResponseGetterOptions( - String apiKey, + Authenticator authenticator, String clientId, int connectTimeout, int readTimeout, @@ -31,8 +33,10 @@ public TestStripeResponseGetterOptions( PasswordAuthentication proxyCredential, String apiBase, String filesBase, - String connectBase) { - this.apiKey = apiKey; + String connectBase, + String meterEventsBase, + String stripeContext) { + this.authenticator = authenticator; this.clientId = clientId; this.connectTimeout = connectTimeout; this.readTimeout = readTimeout; @@ -42,5 +46,7 @@ public TestStripeResponseGetterOptions( this.apiBase = apiBase; this.filesBase = filesBase; this.connectBase = connectBase; + this.meterEventsBase = meterEventsBase; + this.stripeContext = stripeContext; } } diff --git a/src/test/java/com/stripe/net/WebhookTest.java b/src/test/java/com/stripe/net/WebhookTest.java index 174ebd987fe..70200718b11 100644 --- a/src/test/java/com/stripe/net/WebhookTest.java +++ b/src/test/java/com/stripe/net/WebhookTest.java @@ -36,7 +36,7 @@ public void setUpFixtures() { payload = "{\n \"id\": \"evt_test_webhook\",\n \"object\": \"event\"\n}"; } - public String generateSigHeader() throws NoSuchAlgorithmException, InvalidKeyException { + public static String generateSigHeader() throws NoSuchAlgorithmException, InvalidKeyException { final Map options = new HashMap<>(); return generateSigHeader(options); } @@ -47,7 +47,7 @@ public String generateSigHeader() throws NoSuchAlgorithmException, InvalidKeyExc * @param options Options map to override default values * @return The contents of the generated header */ - public String generateSigHeader(Map options) + public static String generateSigHeader(Map options) throws NoSuchAlgorithmException, InvalidKeyException { final long timestamp = (options.get("timestamp") != null) @@ -285,7 +285,7 @@ public void testStripeClientConstructEvent() options.put("payload", payload); final String sigHeader = generateSigHeader(options); - final Event event = client.constructEvent(payload, sigHeader, secret); + final Event event = client.parseSnapshotEvent(payload, sigHeader, secret); final Reader reader = (Reader) event.getDataObjectDeserializer().getObject().get(); reader.delete(); @@ -318,7 +318,7 @@ public void testStripeClientConstructEventWithTolerance() options.put("payload", payload); final String sigHeader = generateSigHeader(options); - final Event event = client.constructEvent(payload, sigHeader, secret, 500); + final Event event = client.parseSnapshotEvent(payload, sigHeader, secret, 500); final Reader reader = (Reader) event.getDataObjectDeserializer().getObject().get(); reader.delete(); diff --git a/src/test/java/com/stripe/v2/AmountTest.java b/src/test/java/com/stripe/v2/AmountTest.java new file mode 100644 index 00000000000..5b532dc12d1 --- /dev/null +++ b/src/test/java/com/stripe/v2/AmountTest.java @@ -0,0 +1,35 @@ +package com.stripe.v2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.stripe.BaseStripeTest; +import com.stripe.net.ApiResource; +import org.junit.jupiter.api.Test; + +public class AmountTest extends BaseStripeTest { + @Test + public void testDeserialize() throws Exception { + final Amount amount = + ApiResource.GSON.fromJson("{\"value\": 10, \"currency\": \"USD\"}", Amount.class); + assertNotNull(amount); + assertEquals(10, amount.getValue()); + assertEquals("USD", amount.getCurrency()); + } + + @Test + public void testDeserializeExtra() throws Exception { + final Amount amount = + ApiResource.GSON.fromJson( + "{\"value\": 10, \"currency\": \"USD\", \"extra\": 42}", Amount.class); + assertNotNull(amount); + assertEquals(10, amount.getValue()); + assertEquals("USD", amount.getCurrency()); + } + + @Test + public void testSerialize() throws Exception { + final String amountJson = ApiResource.GSON.toJson(new Amount(10, "USD"), Amount.class); + assertEquals("{\"value\":10,\"currency\":\"USD\"}", amountJson); + } +} diff --git a/src/test/java/com/stripe/v2/InstantTest.java b/src/test/java/com/stripe/v2/InstantTest.java new file mode 100644 index 00000000000..cb5e124823e --- /dev/null +++ b/src/test/java/com/stripe/v2/InstantTest.java @@ -0,0 +1,38 @@ +package com.stripe.v2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.stripe.net.ApiResource; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +public class InstantTest { + @Test + public void testDeserialize() { + final Instant instant = + ApiResource.GSON.fromJson("\"2023-03-01T22:08:48.920Z\"", Instant.class); + assertNotNull(instant); + assertEquals(Instant.parse("2023-03-01T22:08:48.920Z"), instant); + } + + @Test + public void testDeserializeNull() { + final Instant instant = ApiResource.GSON.fromJson("null", Instant.class); + assertNull(instant); + } + + @Test + public void testSerialize() { + final String instantJson = + ApiResource.GSON.toJson(Instant.parse("2023-03-01T22:08:48.920Z"), Instant.class); + assertEquals("\"2023-03-01T22:08:48.920Z\"", instantJson); + } + + @Test + public void testSerializeNull() { + final String instantJson = ApiResource.GSON.toJson(null, Instant.class); + assertEquals("null", instantJson); + } +} diff --git a/src/test/resources/api_fixtures/billing_meter.json b/src/test/resources/api_fixtures/billing_meter.json new file mode 100644 index 00000000000..c64443af045 --- /dev/null +++ b/src/test/resources/api_fixtures/billing_meter.json @@ -0,0 +1,25 @@ +{ + "id": "meter_123", + "object": "billing.meter", + "created": 1727303036, + "customer_mapping": { + "event_payload_key": "e1", + "type": "by_id" + }, + "default_aggregation": { + "formula": "sum" + }, + + "display_name": "API Requests", + "event_name": "API Request Made", + "event_time_window": "day", + "livemode": false, + "status": "active", + "status_transitions": { + "deactivated_at": null + }, + "updated": 1727303036, + "value_settings": { + "event_payload_key": "e1" + } +} diff --git a/src/test/resources/api_fixtures/error_v2_outbound_payment_insufficient_funds.json b/src/test/resources/api_fixtures/error_v2_outbound_payment_insufficient_funds.json new file mode 100644 index 00000000000..c235ffb09c2 --- /dev/null +++ b/src/test/resources/api_fixtures/error_v2_outbound_payment_insufficient_funds.json @@ -0,0 +1,8 @@ +{ + "error": { + "type": "temporary_session_expired", + "code": "does not matter", + "message": "Session expired", + "request_log_url": "https://dashboard.stripe.com/logs/req_xyz" + } +} diff --git a/src/test/resources/api_fixtures/external_account_collection.json b/src/test/resources/api_fixtures/external_account_collection.json index 7e30873ca1d..d2afe8ea1ca 100644 --- a/src/test/resources/api_fixtures/external_account_collection.json +++ b/src/test/resources/api_fixtures/external_account_collection.json @@ -7,7 +7,8 @@ }, { "id": "ba_123", - "object": "bank_account" + "object": "bank_account", + "account": "acc_123" }, { "id": "bar_123", diff --git a/src/test/resources/api_fixtures/financial_account.json b/src/test/resources/api_fixtures/financial_account.json new file mode 100644 index 00000000000..6af50b7ed9f --- /dev/null +++ b/src/test/resources/api_fixtures/financial_account.json @@ -0,0 +1,10 @@ +{ + "id": "fa_123", + "object": "financial_account", + "description": "test account", + "created": "2024-09-16T23:34:07Z", + "country": "US", + "balance_types": ["payments"], + "requested_currencies": ["USD"], + "status": "open" +}