diff --git a/.github/workflows/hndrs-gradle-check.yml b/.github/workflows/hndrs-gradle-check.yml new file mode 100644 index 0000000..fd9dde2 --- /dev/null +++ b/.github/workflows/hndrs-gradle-check.yml @@ -0,0 +1,38 @@ +name: gradle + +# Controls when the action will run. +on: + pull_request: + branches: [ main ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: git checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: setup java + uses: actions/setup-java@v1 + with: + java-version: '11' + + - name: gradle cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: test + run: | + ./gradlew check diff --git a/.github/workflows/hndrs-gradle-publish.yml b/.github/workflows/hndrs-gradle-publish.yml new file mode 100644 index 0000000..83437a6 --- /dev/null +++ b/.github/workflows/hndrs-gradle-publish.yml @@ -0,0 +1,44 @@ +name: gradle + +# Controls when the action will run. +on: + push: + tags: + - v* + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: git checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: setup java + uses: actions/setup-java@v1 + with: + java-version: '11' + + - name: setup build cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: publish + env: + SONATYPE_USER: ${{ secrets.SONATYPE_USER }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + run: | + ./gradlew publish diff --git a/.github/workflows/hndrs-gradle-sonar.yml b/.github/workflows/hndrs-gradle-sonar.yml new file mode 100644 index 0000000..162ae43 --- /dev/null +++ b/.github/workflows/hndrs-gradle-sonar.yml @@ -0,0 +1,51 @@ +name: gradle + +# Controls when the action will run. +on: + push: + branches: + - main + pull_request: + branches: [ main ] + types: [ opened, synchronize, reopened ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + analyse: + runs-on: ubuntu-latest + + steps: + - name: git checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: setup java + uses: actions/setup-java@v1 + with: + java-version: '11' + + - name: gradle cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: sonar cache + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: analyse + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew check jacocoTestReport sonarqube --info diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c483a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +.DS_Store +.gradle +**/build/ +!gradle/wrapper/gradle-wrapper.jar + + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +**/out/ + +### NetBeans ### +/nbproject/private/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/% diff --git a/GETTING_STARTED_GUIDE.md b/GETTING_STARTED_GUIDE.md new file mode 100644 index 0000000..a0d45fa --- /dev/null +++ b/GETTING_STARTED_GUIDE.md @@ -0,0 +1,109 @@ +### What You Will Build + +You will build an application that receives a subscription update stripe event by using ```StripeEventHandler```. + +### What You Need + +- About 15 minutes +- A favorite text editor or IDE +- JDK 11 or later +- Gradle + +### Starting with Spring Initializr + +For all Spring applications, you should start with the [Spring Initializr](https://start.spring.io/). The Initializr +offers a fast way to pull in all the dependencies you need for an application and does a lot of the set up for you. This +example needs only the Spring Web dependency, Java 11 and Gradle + +Add the following to your ```build.gradle.kts``` or ```build.gradle``` + +```kotlin +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.springframework.boot") version "2.4.2" + id("io.spring.dependency-management") version "1.0.11.RELEASE" + kotlin("jvm") version "1.4.21" + kotlin("plugin.spring") version "1.4.21" +} + +repositories { + mavenCentral() +} + +dependencies { + // add the stripe spring boot starter to your gradle build file + implementation("io.hndrs:stripe-spring-boot-starter:1.0.0") + + // add the stripe java library + implementation("com.stripe:stripe-java:20.37.0") + + + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "11" + } +} + +tasks.withType { + useJUnitPlatform() +} + + +``` + +### Configure the stripe webhook + +To configure the stripe webhook we need to set its ```signingSecret``` and the ```webhook-path```. Add the following +properties to ```src/main/resources/application``` + +```properties +hndrs.stripe.signingSecret=whsc_********** +hndrs.stripe.webhook-path=/stripe-events +``` + +> The signing secret can be obtained on your [Stripe Dashboard](https://dashboard.stripe.com/test/webhooks) or with the [Stripe CLI](https://stripe.com/docs/stripe-cli/webhooks) + +### Create a Stripe Event Handler + +With any webhook-event-based application, you need to create a receiver that responds to published webhook events. The +following implementation shows how to do so: + +```kotlin +@Component +open class ExampleReceiver : StripeEventReceiver(Subscription::class.java) { + + override fun onCondition(event: Event): Boolean { + // check the event type + return event.type == "customer.subscription.updated" + } + + companion object { + private val LOG = LoggerFactory.getLogger(ExampleReceiver::class.java) + } +} +``` + +### Send a Test Message + +Use the [Stripe Cli](https://stripe.com/docs/stripe-cli/webhooks) to send a test event. + +#### Listen for events + +```shell +stripe listen +``` + +#### Trigger an event + +```shell +stripe trigger customer.subscription.updated +``` diff --git a/LICENSE b/LICENSE index 6c53d02..3024013 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 hndrs +Copyright (c) 2021 Marvin Schramm (https://github.com/MarvinSchramm) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..7526b59 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +[![Current Version](https://img.shields.io/maven-central/v/io.hndrs/hndrs_stripe-spring-boot-starter?style=for-the-badge&logo=sonar)](https://search.maven.org/search?q=io.hndrs) +[![Coverage](https://img.shields.io/sonar/coverage/hndrs_stripe-spring-boot-starter?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge)](https://sonarcloud.io/dashboard?id=hndrs_stripe-spring-boot-starter) +[![Supported Java Version](https://img.shields.io/badge/Supported%20Java%20Version-11%2B-informational?style=for-the-badge)]() +[![License](https://img.shields.io/github/license/hndrs/stripe-spring-boot-starter?style=for-the-badge)]() +[![Sponsor](https://img.shields.io/static/v1?logo=GitHub&label=Sponsor&message=%E2%9D%A4&color=ff69b4&style=for-the-badge)](https://github.com/sponsors/marvinschramm) + +# stripe-spring-boot-starter + +Follow the [Getting Started Guide](GETTING_STARTED_GUIDE.md) or look at the [Sample](/sample) to help setting up +stripe-spring-boot-starter. + +#### Dependency + +```kotlin +implementation("io.hndrs:stripe-spring-boot-starter:1.0.0") + +//skip this if you already have the stripe dependency in your project +implementation("com.stripe:stripe-java:") +``` + +> Gradle + +#### Configuration + +```properties +hndrs.stripe.signing-secret=whsec_******************* +hndrs.stripe.webhook-path=/stripe-events +``` + +> application.properties + +#### StripeEventReceiver + +There are 3 conditional methods that can be used to narrow the execution condition (Note: there is an +internal ```class``` conditional that makes sure that the receiver only receives the defined generic type. You can +override any of the following methods (by default they return true) + +- ```onCondition(event: Event)``` + - *It is recommended to use this conditional to check at least the event type* +- ```onReceive(stripeObject: Subscription)``` + - *It is recommended to use this when your condition **only** needs values from the ```stripeObject``` for your + business logic +- ```onCondition(previousAttributes: Map?)``` + - *It is recommended to use this conditional when your condition needs **only** values from + the ```previousAttributes```* +- ```onCondition(previousAttributes: Map?, stripeObject: Subscription)``` + - *It is recommended to use this conditional when your condition needs a combination of the ```previousAttributes``` + and the received ```stripeObject```* + +Implementing a ```StripeEventReceiver``` looks like the following: + +```kotlin +@Component +open class ExampleReceiver : StripeEventReceiver(Subscription::class.java) { + + override fun onCondition(event: Event): Boolean { + // conditional based stripe event + return event.type == "customer.subscription.updated" + } + + override fun onCondition(stripeObject: Subscription): Boolean { + // conditional based stripe object + return true + } + + override fun onCondition(previousAttributes: Map): Boolean { + // conditional based on previousAttributes + return true + } + + override fun onCondition(previousAttributes: Map, stripeObject: Subscription): Boolean { + // conditional based previousAttributes and stripe object + return true + } + + override fun onReceive(stripeObject: Subscription) { + // do something with the received object + } +} +``` + +> The ```StripeEventReceiver``` generic needs to be subclass of a [StripeObject](https://github.com/stripe/stripe-java/blob/master/src/main/java/com/stripe/model/StripeObject.java) + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..95bdcd7 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,116 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + + +buildscript { + repositories { + mavenCentral() + maven(url = "https://repo.spring.io/plugins-release") + } + dependencies { + classpath("io.spring.gradle:propdeps-plugin:0.0.9.RELEASE") + } +} +apply(plugin = "propdeps") +apply(plugin = "propdeps-idea") + +val springBootDependencies: String by extra +val kotlinVersion: String by extra + +plugins { + id("org.sonarqube").version("3.1.1") + id("io.spring.dependency-management") + kotlin("jvm") + kotlin("plugin.spring") + kotlin("kapt") + id("maven-publish") + id("idea") + id("signing") + id("io.hndrs.publishing-info").version("1.1.0").apply(false) +} + +group = "io.hndrs" +version = "1.0.0-1" +java.sourceCompatibility = JavaVersion.VERSION_11 + + +repositories { + mavenCentral() +} + +sonarqube { + properties { + property("sonar.projectKey", "hndrs_stripe-spring-boot-starter") + property("sonar.organization", "hndrs") + property("sonar.host.url", "https://sonarcloud.io") + property("sonar.exclusions", "**/sample/**") + } +} + +subprojects { + apply(plugin = "kotlin") + apply(plugin = "java-library") + apply(plugin = "java") + apply(plugin = "io.spring.dependency-management") + apply(plugin = "kotlin-kapt") + apply(plugin = "propdeps") + apply(plugin = "propdeps-idea") + apply(plugin = "jacoco") + apply(plugin = "maven-publish") + apply(plugin = "signing") + apply(plugin = "io.hndrs.publishing-info") + + configure { + toolVersion = "0.8.6" + } + + tasks.withType { + reports { + xml.apply { + isEnabled = true + } + + } + } + + tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "11" + } + } + + tasks.withType { + useJUnitPlatform() + } + + dependencies { + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + } + + dependencyManagement { + resolutionStrategy { + cacheChangingModulesFor(0, "seconds") + } + imports { + mavenBom("org.springframework.boot:spring-boot-dependencies:$springBootDependencies") { + bomProperty("kotlin.version", kotlinVersion) + } + } + } + + publishing { + repositories { + maven { + name = "release" + url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2") + credentials { + username = System.getenv("SONATYPE_USER") + password = System.getenv("SONATYPE_PASSWORD") + } + } + } + + } +} + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..668e04f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +kotlinVersion=1.4.30 +springDependencyManagement=1.0.11.RELEASE +springBootDependencies=2.4.2 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..be52383 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts new file mode 100644 index 0000000..0e24ebe --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("org.springframework.boot") version "2.4.2" +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":starter")) + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.stripe:stripe-java:20.37.0") +} + +sonarqube { + isSkipProject = true +} diff --git a/sample/src/main/kotlin/io/hndrs/stripe/sample/ExampleReceiver.kt b/sample/src/main/kotlin/io/hndrs/stripe/sample/ExampleReceiver.kt new file mode 100644 index 0000000..cbef5cf --- /dev/null +++ b/sample/src/main/kotlin/io/hndrs/stripe/sample/ExampleReceiver.kt @@ -0,0 +1,39 @@ +package io.hndrs.stripe.sample + +import com.stripe.model.Event +import com.stripe.model.Subscription +import io.hndrs.stripe.StripeEventReceiver +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +open class ExampleReceiver : StripeEventReceiver(Subscription::class.java) { + + override fun onCondition(event: Event): Boolean { + // conditional based stripe event + return event.type == "customer.subscription.updated" + } + + override fun onCondition(stripeObject: Subscription): Boolean { + // conditional based stripe object + return true + } + + override fun onCondition(previousAttributes: Map?): Boolean { + // conditional based previousAttributes + return true + } + + override fun onCondition(previousAttributes: Map?, stripeObject: Subscription): Boolean { + // conditional based previousAttributes and stripe object + return true + } + + override fun onReceive(stripeObject: Subscription, event: Event) { + LOG.info("Received event {}", event) + } + + companion object { + private val LOG = LoggerFactory.getLogger(ExampleReceiver::class.java) + } +} diff --git a/sample/src/main/kotlin/io/hndrs/stripe/sample/StripeWebhookApplication.kt b/sample/src/main/kotlin/io/hndrs/stripe/sample/StripeWebhookApplication.kt new file mode 100644 index 0000000..b6f6c3c --- /dev/null +++ b/sample/src/main/kotlin/io/hndrs/stripe/sample/StripeWebhookApplication.kt @@ -0,0 +1,12 @@ +package io.hndrs.stripe.sample + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +open class StripeWebhookApplication { +} + +fun main(args: Array) { + runApplication(*args) +} diff --git a/sample/src/main/resources/application.properties b/sample/src/main/resources/application.properties new file mode 100644 index 0000000..ade9fad --- /dev/null +++ b/sample/src/main/resources/application.properties @@ -0,0 +1,2 @@ +hndrs.stripe.signing-secret=whsec_lWELWKtHYDW32WIjPE5LWyqUmXqVZcjb +hndrs.stripe.webhook-path=/stripe-events diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8a88018 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,25 @@ +rootProject.name = "stripe-spring-boot-starter" + +include("starter") +project(":starter").projectDir = File("starter") + +include("sample") +project(":sample").projectDir = File("sample") + +val springBootDependencies: String by settings + +pluginManagement { + val kotlinVersion: String by settings + val springDependencyManagement: String by settings + + plugins { + id("io.spring.dependency-management").version(springDependencyManagement) + kotlin("jvm").version(kotlinVersion) + kotlin("plugin.spring").version(kotlinVersion) + kotlin("kapt").version(kotlinVersion) + id("maven-publish") + id("idea") + } + repositories { + } +} diff --git a/starter/build.gradle.kts b/starter/build.gradle.kts new file mode 100644 index 0000000..608dfad --- /dev/null +++ b/starter/build.gradle.kts @@ -0,0 +1,70 @@ +import io.hndrs.gradle.plugin.publishingInfo + +repositories { + mavenCentral() +} + +dependencies { + api(group = "org.springframework.boot", name = "spring-boot-autoconfigure") + optional("org.springframework.boot:spring-boot-starter-web") + optional("com.stripe:stripe-java:20.37.0") + + annotationProcessor(group = "org.springframework.boot", name = "spring-boot-configuration-processor") + kapt(group = "org.springframework.boot", name = "spring-boot-configuration-processor") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("com.ninja-squad:springmockk:3.0.1") + testImplementation("org.junit-pioneer:junit-pioneer:1.3.0") +} + +publishingInfo { + name = rootProject.name + description = "Stripe webhook for spring boot" + url = "https://github.com/hndrs/stripe-spring-boot-starter" + license = io.hndrs.gradle.plugin.License( + "https://github.com/hndrs/stripe-spring-boot-starter/blob/main/LICENSE", + "MIT License" + ) + developers = listOf( + io.hndrs.gradle.plugin.Developer("marvinschramm", "Marvin Schramm", "marvin.schramm@gmail.com") + ) + contributers = listOf( + io.hndrs.gradle.plugin.Contributor("Kevin Joffe", "") + ) + organization = io.hndrs.gradle.plugin.Organization("hndrs", "https://oss.hndrs.io") + scm = io.hndrs.gradle.plugin.Scm( + "scm:git:git://github.com/hndrs/stripe-spring-boot-starter", + "https://github.com/hndrs/stripe-spring-boot-starter" + ) +} + +val sourcesJarSubProject by tasks.creating(Jar::class) { + dependsOn("classes") + archiveClassifier.set("sources") + from(sourceSets["main"].allSource) +} + +java { + withJavadocJar() +} + +publishing { + publications { + create(project.name) { + from(components["java"]) + artifact(sourcesJarSubProject) + + groupId = rootProject.group as? String + artifactId = rootProject.name + version = "${rootProject.version}${project.findProperty("version.appendix") ?: ""}" + } + } + val signingKey: String? = System.getenv("SIGNING_KEY") + val signingPassword: String? = System.getenv("SIGNING_PASSWORD") + if (signingKey != null && signingPassword != null) { + signing { + useInMemoryPgpKeys(groovy.json.StringEscapeUtils.unescapeJava(signingKey), signingPassword) + sign(publications[project.name]) + } + } +} diff --git a/starter/src/main/kotlin/io/hdnrs/autoconfiguration/StripeWebhookAutoConfiguration.kt b/starter/src/main/kotlin/io/hdnrs/autoconfiguration/StripeWebhookAutoConfiguration.kt new file mode 100644 index 0000000..7c751ce --- /dev/null +++ b/starter/src/main/kotlin/io/hdnrs/autoconfiguration/StripeWebhookAutoConfiguration.kt @@ -0,0 +1,43 @@ +package io.hdnrs.autoconfiguration + +import com.stripe.Stripe +import com.stripe.model.StripeObject +import io.hdnrs.autoconfiguration.StripeConfigurationProperties.Companion.PROPERTY_PREFIX +import io.hndrs.stripe.StripeEventReceiver +import io.hndrs.stripe.StripeEventWebhook +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@ConditionalOnWebApplication +@EnableConfigurationProperties(StripeConfigurationProperties::class) +@Configuration +@ConditionalOnClass(Stripe::class) +open class StripeWebhookAutoConfiguration(private val properties: StripeConfigurationProperties) { + + + @Bean + open fun stripeEventWebhook(stripeEventReceivers: List>): StripeEventWebhook { + return StripeEventWebhook( + stripeEventReceivers as List>, + properties.signingSecret + ) + } + +} + +@ConfigurationProperties(PROPERTY_PREFIX) +class StripeConfigurationProperties { + + lateinit var signingSecret: String + + lateinit var webhookPath: String + + companion object { + const val PROPERTY_PREFIX = "hndrs.stripe" + } +} diff --git a/starter/src/main/kotlin/io/hndrs/stripe/StripeEventWebhook.kt b/starter/src/main/kotlin/io/hndrs/stripe/StripeEventWebhook.kt new file mode 100644 index 0000000..d10c0e7 --- /dev/null +++ b/starter/src/main/kotlin/io/hndrs/stripe/StripeEventWebhook.kt @@ -0,0 +1,169 @@ +package io.hndrs.stripe + +import com.fasterxml.jackson.annotation.JsonProperty +import com.stripe.exception.SignatureVerificationException +import com.stripe.model.Event +import com.stripe.model.StripeObject +import com.stripe.net.Webhook +import org.slf4j.LoggerFactory +import org.springframework.http.HttpHeaders +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RestController +import kotlin.reflect.KClass + +@RestController +class StripeEventWebhook( + private val stripeEventReceivers: List>, + private val signingSecret: String, + private val eventBuilder: StripeEventBuilder = StripeEventBuilder(), +) { + + companion object { + private val LOG = LoggerFactory.getLogger(StripeEventWebhook::class.java) + } + + @PostMapping("\${hndrs.stripe.webhook-path}") + fun stripeEvents( + @RequestHeader httpHeaders: HttpHeaders, + @RequestBody body: String + ): ResponseEntity<*> { + LOG.debug("Received event: {}", body) + + val sigHeader = httpHeaders["stripe-signature"]?.firstOrNull().orEmpty() + + LOG.debug("Signature: {}", sigHeader) + // verify signing secret and construct event + val event = try { + eventBuilder.constructEvent( + body, sigHeader, signingSecret + ) + } catch (e: SignatureVerificationException) { + // Invalid signature + LOG.error("Failed to verify stripe signature", e) + return ResponseEntity.badRequest().body("Signature Verification failed") + } catch (e: Exception) { + LOG.error("Failed to handle event callback", e) + return ResponseEntity.badRequest().body("Event handling failed") + } + + val exceptions = mutableMapOf, Exception>() + val results = mutableMapOf, Any?>() + + val stripeObject = event.dataObjectDeserializer.deserializeUnsafe() + stripeEventReceivers.stream() + .forEach { eventReceiver -> + try { + val evaluationReport = eventReceiver.onCondition( + stripeObject.javaClass, + event, + event.data.previousAttributes, + stripeObject + ) + LOG.debug("{} evaluation report: {}", eventReceiver::class.simpleName, evaluationReport) + if (evaluationReport.evaluate()) { + val result = eventReceiver.onReceive(stripeObject, event) + results[eventReceiver::class] = result + } + } catch (e: Exception) { + exceptions[eventReceiver::class] = e + LOG.error("Error while executing {}", eventReceiver::class.java.canonicalName) + } + } + + return ResponseEntity.ok( + ReceiverExecution.of(results, exceptions) + ) + } +} + +data class ReceiverExecution( + @field:JsonProperty("name") + val name: String, + @field:JsonProperty("result") + val result: Any?, + @field:JsonProperty("exceptionMessage") + val exceptionMessage: String? +) { + + companion object { + fun of( + results: MutableMap, Any?>, + exceptions: MutableMap, Exception> + ): List { + return (results.keys + exceptions.keys) + .map { + val name = it.simpleName ?: "Anonymous" + ReceiverExecution(name, results[it], exceptions[it]?.message) + } + } + } +} + +/** + * Delegate class introduced to give the possiblibity to test [StripeEventWebhook] + */ +class StripeEventBuilder { + + fun constructEvent(payload: String, signature: String, signingSecret: String): Event { + return Webhook.constructEvent( + payload, signature, signingSecret + ) + } +} + +abstract class StripeEventReceiver(private val clazz: Class) { + + /** + * Conditional to execute [StripeEventReceiver][onReceive] + */ + open fun onCondition(event: Event): Boolean { + return true + } + + /** + * Conditional to execute [StripeEventReceiver][onReceive] + */ + open fun onCondition(stripeObject: T): Boolean { + return true + } + + /** + * Conditional to execute [StripeEventReceiver][onReceive] + */ + open fun onCondition(previousAttributes: Map?): Boolean { + return true + } + + /** + * Conditional to execute [StripeEventReceiver][onReceive] + */ + open fun onCondition(previousAttributes: Map?, stripeObject: T): Boolean { + return true + } + + /** + * internal support checks + */ + internal fun onCondition(type: Class, event: Event, previousAttributes: Map?, stripeObject: T): ConditionEvaluationReport { + return ConditionEvaluationReport( + type == clazz, onCondition(event), onCondition(previousAttributes), onCondition(stripeObject), onCondition(previousAttributes, stripeObject) + ) + } + + abstract fun onReceive(stripeObject: T, event: Event): Any? + + data class ConditionEvaluationReport( + val onClass: Boolean, + val onEvent: Boolean, + val onPreviousAttributes: Boolean, + val onStripeObject: Boolean, + val onPreviousAttributesAndStripeObject: Boolean, + ) { + fun evaluate(): Boolean { + return onClass && onEvent && onPreviousAttributes && onStripeObject && onStripeObject && onPreviousAttributesAndStripeObject + } + } +} diff --git a/starter/src/main/resources/META-INF/spring.factories b/starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..67d5a09 --- /dev/null +++ b/starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + io.hdnrs.autoconfiguration.StripeWebhookAutoConfiguration diff --git a/starter/src/test/kotlin/io/hndrs/autoconfiguration/AutoConfigurationTests.kt b/starter/src/test/kotlin/io/hndrs/autoconfiguration/AutoConfigurationTests.kt new file mode 100644 index 0000000..86fd12d --- /dev/null +++ b/starter/src/test/kotlin/io/hndrs/autoconfiguration/AutoConfigurationTests.kt @@ -0,0 +1,42 @@ +package io.hndrs.autoconfiguration + +import com.stripe.Stripe +import io.hdnrs.autoconfiguration.StripeWebhookAutoConfiguration +import io.hndrs.stripe.StripeEventWebhook +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.NoSuchBeanDefinitionException +import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.test.context.FilteredClassLoader +import org.springframework.boot.test.context.runner.WebApplicationContextRunner + +@DisplayName("Stripe Webhook Autoconfiguration") +class StripeWebhookAutoConfigurationTests { + + @Test + fun autoconfiguredBeans() { + WebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(StripeWebhookAutoConfiguration::class.java) + ) + .withPropertyValues("hndrs.stripe.signing-secret:testSecret", "hndrs.stripe.webhook-path:/events") + .run { + assertNotNull(it.getBean(StripeEventWebhook::class.java)) + } + } + + @Test + fun autoconfiguredMissingStripeClass() { + WebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(StripeWebhookAutoConfiguration::class.java) + ) + .withClassLoader(FilteredClassLoader(Stripe::class.java)) + .withPropertyValues("hndrs.stripe.signing-secret:testSecret", "hndrs.stripe.webhook-path:/events") + .run { + assertThrows(NoSuchBeanDefinitionException::class.java) { it.getBean(StripeEventWebhook::class.java) } + } + } +} diff --git a/starter/src/test/kotlin/io/hndrs/stripe/StripeEventWebhookTest.kt b/starter/src/test/kotlin/io/hndrs/stripe/StripeEventWebhookTest.kt new file mode 100644 index 0000000..f296414 --- /dev/null +++ b/starter/src/test/kotlin/io/hndrs/stripe/StripeEventWebhookTest.kt @@ -0,0 +1,313 @@ +package io.hndrs.stripe + +import com.stripe.exception.SignatureVerificationException +import com.stripe.model.Event +import com.stripe.model.EventData +import com.stripe.model.EventDataObjectDeserializer +import com.stripe.model.Invoice +import com.stripe.model.StripeObject +import com.stripe.model.Subscription +import io.hndrs.stripe.StripeEventReceiver.ConditionEvaluationReport +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junitpioneer.jupiter.CartesianProductTest +import org.springframework.http.HttpHeaders +import org.springframework.http.ResponseEntity + +@DisplayName("Stripe Event Webhook") +internal class StripeEventWebhookTest { + + private val eventBuilder = mockk(relaxed = true) + + companion object { + private const val TEST_BODY = "" + + @JvmStatic + fun booleanFactory(): CartesianProductTest.Sets { + return CartesianProductTest.Sets() + .add(true, false) + .add(true, false) + .add(true, false) + .add(true, false) + .add(true, false) + } + } + + private fun testWebHook(stripeEventReceiver: StripeEventReceiver<*>? = null): StripeEventWebhook { + return stripeEventReceiver?.let { + StripeEventWebhook(listOf(stripeEventReceiver as StripeEventReceiver), "", eventBuilder) + } ?: StripeEventWebhook(listOf(), "", eventBuilder) + } + + @BeforeEach + fun setup() { + clearMocks(eventBuilder) + } + + @DisplayName("Missing or Invalid Signature") + @Test + fun signatureVerificationFailed() { + every { eventBuilder.constructEvent(any(), any(), any()) } throws SignatureVerificationException( + "message", + "sigheader" + ) + + assertEquals( + ResponseEntity.badRequest().body("Signature Verification failed"), + testWebHook().stripeEvents(HttpHeaders(), TEST_BODY) + ) + } + + @DisplayName("Any exception during event construction") + @Test + fun anyOtherException() { + every { eventBuilder.constructEvent(any(), any(), any()) } throws IllegalStateException() + + assertEquals( + ResponseEntity.badRequest().body("Event handling failed"), + testWebHook().stripeEvents(HttpHeaders(), TEST_BODY) + ) + } + + @DisplayName("Any exception during supports check") + @Test + fun previousAttributesNull() { + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk(), previousAttributes = null) + + val ex = IllegalStateException() + val throwsOnSupport = ThrowsOnSupport(ex) + + assertEquals( + ResponseEntity.ok(listOf(ReceiverExecution(ThrowsOnSupport::class.simpleName!!, null, ex.message))), + testWebHook(throwsOnSupport).stripeEvents(HttpHeaders(), TEST_BODY) + ) + assertFalse(throwsOnSupport.exectuedOnReceive, "onReceive was executed") + } + + @DisplayName("Any exception during supports check") + @Test + fun exceptionDuringOnCondition() { + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk()) + + val ex = IllegalStateException() + val throwsOnSupport = ThrowsOnSupport(ex) + + assertEquals( + ResponseEntity.ok(listOf(ReceiverExecution(ThrowsOnSupport::class.simpleName!!, null, ex.message))), + testWebHook(throwsOnSupport).stripeEvents(HttpHeaders(), TEST_BODY) + ) + assertFalse(throwsOnSupport.exectuedOnReceive, "onReceive was executed") + } + + @DisplayName("Any exception during onReceive call") + @Test + fun exceptionDuringOnReceiveCall() { + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk()) + + val ex = IllegalStateException() + assertEquals( + ResponseEntity.ok( + listOf( + ReceiverExecution( + ThrowsOnReceive::class.simpleName!!, + null, + ex.message + ) + ) + ), + testWebHook(ThrowsOnReceive(ex)).stripeEvents(HttpHeaders(), TEST_BODY) + ) + } + + @DisplayName("onCondtion(Event Class)") + @Test + fun onConditionEventClass() { + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk()) + val invoiceEventHandler = InvoiceEventReceiver() + + assertEquals( + ResponseEntity.ok(listOf()), + testWebHook(invoiceEventHandler).stripeEvents(HttpHeaders(), TEST_BODY) + ) + assertFalse(invoiceEventHandler.exectuedOnReceive) + } + + @DisplayName("onCondtion(Event)") + @Test + fun onConditionEvent() { + val eventType = "someEventType" + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk(), eventType) + + val testReceiver = TestReceiver(onConditionEvent = false) + + assertEquals( + ResponseEntity.ok(listOf()), + testWebHook(testReceiver).stripeEvents(HttpHeaders(), TEST_BODY) + ) + assertFalse(testReceiver.exectuedOnReceive) + } + + @DisplayName("onCondition(previousAttributes Map)") + @Test + fun onConditionPreviousAttributes() { + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk()) + + val testReceiver = TestReceiver(onConditionPreviousAttributes = false) + + assertEquals( + ResponseEntity.ok(listOf()), + testWebHook(testReceiver).stripeEvents(HttpHeaders(), TEST_BODY) + ) + assertFalse(testReceiver.exectuedOnReceive) + } + + @DisplayName("onCondition(stripeObject)") + @Test + fun onConditionStripeObject() { + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk()) + + val testReceiver = TestReceiver(onConditionStripeObject = false) + + assertEquals( + ResponseEntity.ok(listOf()), + testWebHook(testReceiver).stripeEvents(HttpHeaders(), TEST_BODY) + ) + assertFalse(testReceiver.exectuedOnReceive) + } + + @DisplayName("onCondition(previousAttributes Map ,stripeObject)") + @Test + fun onConditionPreviousAttributesAndStripeObject() { + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk()) + + val testReceiver = TestReceiver(onConditionPreviousAttributesAndStripeObject = false) + + assertEquals( + ResponseEntity.ok(listOf()), + testWebHook(testReceiver).stripeEvents(HttpHeaders(), TEST_BODY) + ) + assertFalse(testReceiver.exectuedOnReceive) + } + + @DisplayName("On Receive Method Called") + @Test + fun onReceiveExectuted() { + every { eventBuilder.constructEvent(any(), any(), any()) } returns mockkEvent(mockk()) + + val baseHandler = TestReceiver() + + assertEquals( + ResponseEntity.ok(listOf(ReceiverExecution(TestReceiver::class.simpleName!!, Unit, null))), + testWebHook(baseHandler).stripeEvents(HttpHeaders(), TEST_BODY) + ) + assertTrue(baseHandler.exectuedOnReceive) + } + + @DisplayName("Evaluation Report") + @CartesianProductTest(factory = "booleanFactory") + fun evaluationReport( + onClass: Boolean, + onEvent: Boolean, + onPreviousAttributes: Boolean, + onStripeObject: Boolean, + onPreviousAttributesAndStripeObject: Boolean + ) { + + val conditionEvaluationReport = ConditionEvaluationReport(onClass, onEvent, onPreviousAttributes, onStripeObject, onPreviousAttributesAndStripeObject) + + assertEquals(onClass,conditionEvaluationReport.onClass) + assertEquals(onEvent,conditionEvaluationReport.onEvent) + assertEquals(onPreviousAttributes,conditionEvaluationReport.onPreviousAttributes) + assertEquals(onStripeObject,conditionEvaluationReport.onStripeObject) + assertEquals(onPreviousAttributesAndStripeObject,conditionEvaluationReport.onPreviousAttributesAndStripeObject) + + if (onClass && onEvent && onPreviousAttributes && onStripeObject && onPreviousAttributesAndStripeObject) { + assertTrue(conditionEvaluationReport.evaluate()) + } else { + assertFalse(conditionEvaluationReport.evaluate()) + } + } + + private fun mockkEvent( + stripeObject: StripeObject, + type: String = "anyType", + previousAttributes: Map? = mapOf() + ): Event { + + val event = mockk() + val deserializer = mockk() + val data = mockk() {} + every { event.dataObjectDeserializer } returns deserializer + every { deserializer.deserializeUnsafe() } returns stripeObject + every { event.type } returns type + every { event.data } returns data + every { data.previousAttributes } returns previousAttributes + return event + } + + class ThrowsOnSupport(private val ex: Exception) : StripeEventReceiver(Subscription::class.java) { + + var exectuedOnReceive: Boolean = false + + override fun onCondition(event: Event): Boolean { + throw ex + } + + override fun onReceive(stripeObject: Subscription, event: Event) { + exectuedOnReceive = true + } + } + + class ThrowsOnReceive(private val ex: Exception) : StripeEventReceiver(Subscription::class.java) { + override fun onReceive(stripeObject: Subscription, event: Event) { + throw ex + } + } + + class InvoiceEventReceiver : StripeEventReceiver(Invoice::class.java) { + var exectuedOnReceive: Boolean = false + override fun onReceive(stripeObject: Invoice, event: Event) { + exectuedOnReceive = true + } + } + + class TestReceiver( + private val onConditionEvent: Boolean = true, + private val onConditionStripeObject: Boolean = true, + private val onConditionPreviousAttributes: Boolean = true, + private val onConditionPreviousAttributesAndStripeObject: Boolean = true, + ) : StripeEventReceiver(Subscription::class.java) { + + override fun onCondition(event: Event): Boolean { + return onConditionEvent + } + + override fun onCondition(stripeObject: Subscription): Boolean { + return onConditionStripeObject + } + + override fun onCondition(previousAttributes: Map?): Boolean { + return onConditionPreviousAttributes + } + + override fun onCondition(previousAttributes: Map?, stripeObject: Subscription): Boolean { + return onConditionPreviousAttributesAndStripeObject + } + + var exectuedOnReceive: Boolean = false + + override fun onReceive(stripeObject: Subscription, event: Event) { + exectuedOnReceive = true + } + + } + +}