├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── org │ └── loverde │ └── paymentcard │ ├── CardType.java │ ├── PaymentCardGenerator.java │ ├── PaymentCardGeneratorImpl.java │ ├── Range.java │ └── internal │ └── Objects.java └── test └── java └── org └── loverde └── paymentcard ├── CardTypeTest.java ├── PaymentCardGeneratorTest.java └── RangeTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | .settings 4 | .project 5 | .classpath 6 | bin 7 | build 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 (April 10, 2024) 2 | 3 | * Retargeted at Java 17 + Gradle 8 4 | * No more reliance on the external BuildScripts repository to build this project 5 | * Migrated to JUnit 5 6 | * Added Spotbugs code coverage to the build 7 | * Uses of `java.util.Random` have been replaced by `ThreadLocalRandom` 8 | 9 | BREAKING CHANGES: 10 | 11 | ```java 12 | PaymentCardGenerator.generateByPrefix(int howManyOfEachPrefix, List lengths, Set prefixes) 13 | ``` 14 | now uses a `Set` for `lengths` rather than `List` 15 | 16 | 17 | # 1.0.1 (May 8, 2021) 18 | 19 | There are no code changes in this release. The project has been updated to be compatible with the latest [BuildScripts](https://github.com/kloverde/BuildScripts) and Gradle 7.0. 20 | 21 | 22 | # 1.0 (December 21, 2016) 23 | 24 | * First release 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Kurtis LoVerde 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. Neither the name of the copyright holder nor the names of its 13 | contributors may be used to endorse or promote products derived from 14 | this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PaymentCardGenerator v2.0.0 2 | =========================== 3 | 4 | See LICENSE for this software's licensing terms. 5 | 6 | PaymentCardGenerator is a Java library that aids in testing payment card processing systems. 7 | It generates random payment card numbers so that you don't have to use an actual card. 8 | 9 | Card numbers are generated based on the criteria defined here: 10 | 11 | * https://en.wikipedia.org/wiki/Luhn_algorithm 12 | * https://en.wikipedia.org/wiki/Payment_card_number (as of April 2024) 13 | 14 | 15 | ## Features 16 | 17 | * Supports generation of American Express, VISA, MasterCard and Discover 18 | * Easily extensible to support any type of payment card which uses Luhn validation. All you need to do is add a member to the CardType enumeration. 19 | * Numerous criteria for generating numbers, including by type, quantity, length and prefix 20 | * Future-proof: generate numbers based on your own criteria, even if the library doesn't have knowledge of the latest card number formats 21 | 22 | 23 | ## Build Tasks 24 | 25 | This project is known to build on Gradle 8.4. 26 | 27 | | task | purpose | 28 | |---------------------|----------------------------------------------| 29 | | build | Builds the project | 30 | | check | Runs the tests and code quality checks | 31 | | clean | Removes the `build` directory | 32 | | jars | Builds the source, javadoc and binary jars | 33 | | publishToMavenLocal | Published the jars to your local Maven cache | 34 | 35 | 36 | ## Donations 37 | 38 | https://paypal.me/KurtisLoVerde/5 39 | 40 | Thank you for your support! 41 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | plugins { 8 | id("java") 9 | id("com.github.spotbugs") version "6.0.10" 10 | id("maven-publish") 11 | } 12 | 13 | sourceCompatibility = javaSourceCompatibility 14 | targetCompatibility = javaTargetCompatibility 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | dependencies { 21 | implementation "com.github.spotbugs:spotbugs-annotations:4.8.4" 22 | 23 | testImplementation platform("org.junit:junit-bom:5.10.2") 24 | testImplementation "org.junit.jupiter:junit-jupiter" 25 | testImplementation "org.mockito:mockito-junit-jupiter:5.11.0" 26 | testRuntimeOnly "org.junit.platform:junit-platform-launcher" 27 | } 28 | 29 | def includeManifest = { 30 | manifest { 31 | attributes "Specification-Title": rootProject.name, 32 | "Implementation-Version": version, 33 | "Built-Date": new Date(), 34 | "Built-JDK": System.getProperty("java.version"), 35 | "Target-JDK": targetCompatibility, 36 | "Built-Gradle": gradle.gradleVersion 37 | } 38 | } 39 | 40 | java { 41 | withSourcesJar() 42 | withJavadocJar() 43 | } 44 | 45 | tasks.spotbugsMain { 46 | reports.create("html") { 47 | required = true 48 | outputLocation = file("${project.layout.buildDirectory.get()}/reports/spotbugs.html") 49 | setStylesheet("fancy-hist.xsl") 50 | } 51 | } 52 | 53 | jar { 54 | configure includeManifest 55 | } 56 | 57 | sourcesJar { 58 | configure includeManifest 59 | } 60 | 61 | javadocJar { 62 | configure includeManifest 63 | } 64 | 65 | tasks.register("jars") { 66 | dependsOn( 67 | tasks.jar, 68 | tasks.sourcesJar, 69 | tasks.javadocJar 70 | ) 71 | } 72 | 73 | javadoc.options.addStringOption("Xdoclint:none", "-quiet") 74 | 75 | publishing { 76 | publications { 77 | mavenJava(MavenPublication) { 78 | from project.components.java 79 | artifacts = [ jar, javadocJar, sourcesJar ] 80 | } 81 | } 82 | } 83 | 84 | compileJava.dependsOn clean 85 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | javaSourceCompatibility=1.17 2 | javaTargetCompatibility=1.17 3 | 4 | group=org.loverde 5 | version=2.0.0 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kloverde/java-PaymentCardGenerator/753dd3fbd477033ba57a78852c3d0f710dbbbfb2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name="paymentcardgenerator" 2 | -------------------------------------------------------------------------------- /src/main/java/org/loverde/paymentcard/CardType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PaymentCardGenerator 3 | * https://www.github.com/kloverde/java-PaymentCardGenerator 4 | * 5 | * Copyright (c) 2016 Kurtis LoVerde 6 | * All rights reserved 7 | * 8 | * Donations: https://paypal.me/KurtisLoVerde/5 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions are met: 12 | * 13 | * 1. Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 2. Redistributions in binary form must reproduce the above copyright 16 | * notice, this list of conditions and the following disclaimer in the 17 | * documentation and/or other materials provided with the distribution. 18 | * 3. Neither the name of the copyright holder nor the names of its 19 | * contributors may be used to endorse or promote products derived from 20 | * this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | package org.loverde.paymentcard; 35 | 36 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 37 | 38 | import java.util.ArrayList; 39 | import java.util.Collections; 40 | import java.util.LinkedHashSet; 41 | import java.util.List; 42 | import java.util.Set; 43 | 44 | import static org.loverde.paymentcard.internal.Objects.failIf; 45 | 46 | 47 | /** 48 | * Defines various payment card types, along with a known list of valid lengths and prefixes, according to 49 | * https://en.wikipedia.org/wiki/Payment_card_number 50 | * as of April 2024. 51 | */ 52 | public enum CardType { 53 | AMERICAN_EXPRESS( 54 | prefixesFromRange( 55 | new Range(34, 34), 56 | new Range(37, 37)), 57 | 58 | Set.of(15) 59 | ), 60 | 61 | VISA( 62 | prefixesFromRange(new Range(4, 4)), 63 | Set.of(13, 16, 19) 64 | ), 65 | 66 | MASTERCARD( 67 | prefixesFromRange( 68 | new Range(51, 55), 69 | new Range(2221, 2720)), 70 | 71 | Set.of(16) 72 | ), 73 | 74 | DISCOVER( 75 | prefixesFromRange( 76 | new Range(65, 65), 77 | new Range(644, 649), 78 | new Range(6011, 6011), 79 | new Range(622126, 622925)), 80 | 81 | Set.of(16, 19) 82 | ); 83 | 84 | private final Set prefixes; 85 | private final Set lengths; 86 | 87 | 88 | CardType(final Set prefixes, final Set lengths) { 89 | this.prefixes = prefixes; 90 | this.lengths = lengths; 91 | } 92 | 93 | @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Spotbugs doesn't know that they're unmodifiable") 94 | public Set getPrefixes() { 95 | return prefixes; 96 | } 97 | 98 | @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Spotbugs doesn't know that they're unmodifiable") 99 | public Set getLengths() { 100 | return lengths; 101 | } 102 | 103 | private static Set prefixesFromRange(final Range... ranges) { 104 | failIf(ranges == null || ranges.length == 0, () -> "Ranges is null or empty"); 105 | 106 | final Set prefixes = new LinkedHashSet<>(); 107 | 108 | for (final Range r : ranges) { 109 | if (r != null) { 110 | final List list = new ArrayList<>((int) (r.end() - r.start() + 1)); 111 | 112 | for (long i = r.start(); i <= r.end(); i++) { 113 | list.add(i); 114 | } 115 | 116 | prefixes.addAll(list); 117 | } 118 | } 119 | 120 | return Collections.unmodifiableSet(prefixes); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/org/loverde/paymentcard/PaymentCardGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PaymentCardGenerator 3 | * https://www.github.com/kloverde/java-PaymentCardGenerator 4 | * 5 | * Copyright (c) 2016 Kurtis LoVerde 6 | * All rights reserved 7 | * 8 | * Donations: https://paypal.me/KurtisLoVerde/5 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions are met: 12 | * 13 | * 1. Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 2. Redistributions in binary form must reproduce the above copyright 16 | * notice, this list of conditions and the following disclaimer in the 17 | * documentation and/or other materials provided with the distribution. 18 | * 3. Neither the name of the copyright holder nor the names of its 19 | * contributors may be used to endorse or promote products derived from 20 | * this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | package org.loverde.paymentcard; 35 | 36 | import java.util.List; 37 | import java.util.Map; 38 | import java.util.Set; 39 | 40 | 41 | public interface PaymentCardGenerator { 42 | 43 | /** 44 | * Generates a card number for a given card type. The prefix and length are 45 | * randomly selected from the values defined in {@linkplain CardType}. 46 | * 47 | * @param cardType The type of card number of generate 48 | * @return A card number for the specified card type 49 | */ 50 | String generateByCardType(CardType cardType); 51 | 52 | /** 53 | * Generates multiple card numbers for a given card type. The prefix and length 54 | * are randomly selected from the values defined in {@linkplain CardType}. 55 | * 56 | * @param howMany How many card numbers to generate for the specified card type 57 | * @param cardType The type of card numbers to generate 58 | * @return A list of card numbers for the specified card type 59 | */ 60 | List generateListByCardType(int howMany, CardType cardType); 61 | 62 | /** 63 | * Generates card numbers of given card types. The prefix and length are 64 | * randomly selected from the values defined in {@linkplain CardType}. 65 | * 66 | * @param howManyOfEach How many card numbers to generate for each card type 67 | * @param cardTypes Vararg of card types 68 | * @return A map where the key is the card type and the value is a list of card numbers for that card type 69 | */ 70 | Map> generateMapByCardTypes(int howManyOfEach, CardType... cardTypes); 71 | 72 | /** 73 | * Generates numbers based on a specified prefix and length. This method does not validate the prefix 74 | * or length arguments to determine whether they apply to a known {@linkplain CardType}. This is to 75 | * account for the possibility that the Wikipedia articles this software is based on are incorrect, or 76 | * that this software could be outdated. Known prefix and length options are available in the 77 | * {@linkplain CardType} enum, if you wish to use them. Essentially, this method is a general-purpose 78 | * Luhn number generator. 79 | * 80 | * @param howManyOfEachPrefix How many card numbers to generate for each prefix 81 | * @param lengths Generated card numbers will be of lengths specified by this list 82 | * @param prefixes Generated card numbers will start with values from this set 83 | * @return A map where the key is the prefix and the value is a list of card numbers for that prefix 84 | */ 85 | Map> generateByPrefix(int howManyOfEachPrefix, Set lengths, Set prefixes); 86 | 87 | /** 88 | * Determines whether a number passes Luhn validation 89 | * 90 | * @param num A numeric string ending with a check digit 91 | * @return {@code true} if the number is valid, {@code false} if not 92 | */ 93 | boolean passesLuhnCheck(String num); 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/loverde/paymentcard/PaymentCardGeneratorImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PaymentCardGenerator 3 | * https://www.github.com/kloverde/java-PaymentCardGenerator 4 | * 5 | * Copyright (c) 2016 Kurtis LoVerde 6 | * All rights reserved 7 | * 8 | * Donations: https://paypal.me/KurtisLoVerde/5 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions are met: 12 | * 13 | * 1. Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 2. Redistributions in binary form must reproduce the above copyright 16 | * notice, this list of conditions and the following disclaimer in the 17 | * documentation and/or other materials provided with the distribution. 18 | * 3. Neither the name of the copyright holder nor the names of its 19 | * contributors may be used to endorse or promote products derived from 20 | * this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | package org.loverde.paymentcard; 35 | 36 | import java.util.ArrayList; 37 | import java.util.Arrays; 38 | import java.util.HashMap; 39 | import java.util.List; 40 | import java.util.Map; 41 | import java.util.Objects; 42 | import java.util.Random; 43 | import java.util.Set; 44 | import java.util.concurrent.ThreadLocalRandom; 45 | import java.util.stream.Collectors; 46 | import java.util.stream.IntStream; 47 | import java.util.stream.Stream; 48 | 49 | import static org.loverde.paymentcard.internal.Objects.failIf; 50 | import static org.loverde.paymentcard.internal.Objects.randomItemFromSet; 51 | 52 | 53 | /** 54 | *

55 | * This software aids in testing payment card processing systems by generating random payment card 56 | * numbers which are mathematically valid, so that you don't have to use an actual card. 57 | *

58 | * This class generates payment card numbers based on the criteria defined here: 59 | *

60 | * 61 | * 65 | * 66 | *

67 | * Most if not all of the payment card numbers generated by this software should not be tied to active 68 | * accounts. However, it is theoretically possible that, against all odds, this software could randomly 69 | * generate a payment card number that's in use in the real world. For this reason, you must ensure 70 | * that these card numbers are only used in systems running in test mode, i.e. that a real transaction 71 | * will not be attempted. 72 | *

73 | * 74 | *

75 | * There's no point in trying to use this software for fraudulent purposes. Not only will card numbers 76 | * generated by this software likely not work, but if this software were by coincidence to generate an 77 | * actual active account number, it would be illegal for you to attempt to use it. Of course, you're 78 | * smart enough to know this already. You alone are responsible for what you do with this software. 79 | *

80 | */ 81 | public class PaymentCardGeneratorImpl implements PaymentCardGenerator { 82 | 83 | @Override 84 | public String generateByCardType(final CardType cardType) { 85 | failIf(cardType == null, () -> "Card type is null"); 86 | return generateCardNumber(cardType); 87 | } 88 | 89 | @Override 90 | public List generateListByCardType(final int howMany, final CardType cardType) { 91 | failIf(howMany <= 0, () -> "How many must be greater than zero"); 92 | failIf(cardType == null, () -> "Card type is null"); 93 | 94 | return IntStream.range(0, howMany).mapToObj(i -> generateCardNumber(cardType)).collect(Collectors.toList()); 95 | } 96 | 97 | @Override 98 | public Map> generateMapByCardTypes(final int howManyOfEach, final CardType... cardTypes) { 99 | failIf(howManyOfEach <= 0, () -> "How many of each must be greater than zero"); 100 | failIf(cardTypes == null || cardTypes.length < 1, () -> "Card types is null or empty"); 101 | 102 | final Map> cardNums = new HashMap<>(cardTypes.length); 103 | 104 | removeVarargDuplicates(cardTypes).forEach(cardType -> cardNums.put(cardType, generateListByCardType(howManyOfEach, cardType))); 105 | 106 | return cardNums; 107 | } 108 | 109 | @Override 110 | public Map> generateByPrefix(final int howManyOfEachPrefix, final Set lengths, final Set prefixes) { 111 | failIf(howManyOfEachPrefix <= 0, () -> "How many of each must be greater than zero"); 112 | failIf(lengths == null || lengths.isEmpty(), () -> "No lengths were specified"); 113 | failIf(prefixes == null || prefixes.isEmpty(), () -> "No prefixes were specified"); 114 | 115 | for (final Integer length : lengths) { 116 | failIf(length == null || length < 2, () -> "Invalid length: " + length); 117 | 118 | for (final Long prefix : prefixes) { 119 | failIf(prefix.toString().length() > length, () -> "Prefix (%s) is longer than length (%d)".formatted(prefix.toString(), length)); 120 | failIf(prefix < 1, () -> "Prefix (%s): prefixes must be positive numbers".formatted(prefix.toString())); 121 | } 122 | } 123 | 124 | final Map> cardNums = new HashMap<>(prefixes.size()); 125 | 126 | for (final Long prefix : prefixes) { 127 | final List cardNumsForPrefix = new ArrayList<>(howManyOfEachPrefix); 128 | 129 | for (int i = 0; i < howManyOfEachPrefix; i++) { 130 | cardNumsForPrefix.add(generateCardNumber(prefix, randomItemFromSet(lengths))); 131 | } 132 | 133 | cardNums.put(prefix, cardNumsForPrefix); 134 | } 135 | 136 | return cardNums; 137 | } 138 | 139 | @Override 140 | public boolean passesLuhnCheck(final String num) { 141 | failIf(num == null || num.isEmpty(), () -> "Number is null or empty"); 142 | 143 | final int sum = calculateLuhnSum(num, true); 144 | final int checkDigit = calculateCheckDigit(sum); 145 | 146 | return (sum + checkDigit) % 10 == 0 && Integer.parseInt(num.substring(num.length() - 1)) == checkDigit; 147 | } 148 | 149 | private static String generateCardNumber(final CardType cardType) { 150 | return generateCardNumber( 151 | randomItemFromSet(cardType.getPrefixes()), 152 | randomItemFromSet(cardType.getLengths())); 153 | } 154 | 155 | private static String generateCardNumber(final Long prefix, final int length) { 156 | final StringBuilder num = new StringBuilder(prefix.toString()); 157 | 158 | final int howManyMore = length - num.toString().length() - 1; 159 | final Random random = ThreadLocalRandom.current(); 160 | 161 | for (int i = 0; i < howManyMore; i++) { 162 | num.append(Integer.valueOf(random.nextInt(9))); 163 | } 164 | 165 | num.append(calculateCheckDigit(num.toString())); 166 | 167 | return num.toString(); 168 | } 169 | 170 | private static int calculateCheckDigit(final String str) { 171 | final int sum = calculateLuhnSum(str, false); 172 | return calculateCheckDigit(sum); 173 | } 174 | 175 | private static int calculateCheckDigit(final int luhnSum) { 176 | return (luhnSum * 9) % 10; 177 | } 178 | 179 | private static int calculateLuhnSum(final String str, final boolean hasCheckDigit) { 180 | final int[] luhnNums = new int[str.length()]; 181 | final int start = str.length() - (hasCheckDigit ? 2 : 1); 182 | int sum = 0; 183 | 184 | boolean doubleMe = true; 185 | 186 | for (int i = start; i >= 0; i--) { 187 | final int num = Integer.parseInt(str.substring(i, i + 1)); 188 | 189 | if (doubleMe) { 190 | int x2 = num * 2; 191 | luhnNums[i] = x2 > 9 ? x2 - 9 : x2; 192 | } else { 193 | luhnNums[i] = num; 194 | } 195 | 196 | sum += luhnNums[i]; 197 | doubleMe = !doubleMe; 198 | } 199 | 200 | return sum; 201 | } 202 | 203 | @SafeVarargs 204 | private static Set removeVarargDuplicates(final T... stuff) { 205 | return Stream.ofNullable(stuff) 206 | .flatMap(Arrays::stream) 207 | .filter(Objects::nonNull) 208 | .collect(Collectors.toSet()); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/org/loverde/paymentcard/Range.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PaymentCardGenerator 3 | * https://www.github.com/kloverde/java-PaymentCardGenerator 4 | * 5 | * Copyright (c) 2016 Kurtis LoVerde 6 | * All rights reserved 7 | * 8 | * Donations: https://paypal.me/KurtisLoVerde/5 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions are met: 12 | * 13 | * 1. Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 2. Redistributions in binary form must reproduce the above copyright 16 | * notice, this list of conditions and the following disclaimer in the 17 | * documentation and/or other materials provided with the distribution. 18 | * 3. Neither the name of the copyright holder nor the names of its 19 | * contributors may be used to endorse or promote products derived from 20 | * this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | package org.loverde.paymentcard; 35 | 36 | 37 | import static org.loverde.paymentcard.internal.Objects.failIf; 38 | 39 | 40 | public record Range (long start, long end) { 41 | 42 | public Range { 43 | failIf(start > end, () -> "Start cannot be greater than end"); 44 | } 45 | 46 | public long size() { 47 | return Math.abs(end() - start()) + 1; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/loverde/paymentcard/internal/Objects.java: -------------------------------------------------------------------------------- 1 | package org.loverde.paymentcard.internal; 2 | 3 | import java.util.Iterator; 4 | import java.util.Set; 5 | import java.util.concurrent.ThreadLocalRandom; 6 | import java.util.function.Supplier; 7 | 8 | 9 | public class Objects { 10 | 11 | /** 12 | * Shorthand for IF statements that throw IllegalArgumentException 13 | * @param isFailed The result of the check 14 | * @param iaeMessage Exception message 15 | */ 16 | public static void failIf(final boolean isFailed, final Supplier iaeMessage) { 17 | if (isFailed) { 18 | throw new IllegalArgumentException(iaeMessage.get()); 19 | } 20 | } 21 | 22 | /** 23 | * Return a random item from a set. 24 | * @param set The set to pull from 25 | * @return Random item from the set, or null if the set is empty 26 | * @param Parameterized type of the set 27 | */ 28 | public static T randomItemFromSet(final Set set) { 29 | failIf(set == null, () -> "Set is null"); 30 | 31 | T t = null; 32 | Iterator iter = set.iterator(); 33 | 34 | final int stopAt = ThreadLocalRandom.current().nextInt(set.size()); 35 | 36 | for (int i = 0; i <= stopAt; i++) { 37 | t = iter.next(); 38 | } 39 | 40 | return t; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/org/loverde/paymentcard/CardTypeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PaymentCardGenerator 3 | * https://www.github.com/kloverde/java-PaymentCardGenerator 4 | * 5 | * Copyright (c) 2016 Kurtis LoVerde 6 | * All rights reserved 7 | * 8 | * Donations: https://paypal.me/KurtisLoVerde/5 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions are met: 12 | * 13 | * 1. Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 2. Redistributions in binary form must reproduce the above copyright 16 | * notice, this list of conditions and the following disclaimer in the 17 | * documentation and/or other materials provided with the distribution. 18 | * 3. Neither the name of the copyright holder nor the names of its 19 | * contributors may be used to endorse or promote products derived from 20 | * this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | package org.loverde.paymentcard; 35 | 36 | import org.junit.jupiter.params.ParameterizedTest; 37 | import org.junit.jupiter.params.provider.EnumSource; 38 | 39 | import java.util.*; 40 | 41 | import static org.junit.jupiter.api.Assertions.assertThrows; 42 | 43 | 44 | /** 45 | * These tests verify that the {@linkplain CardType} enum is built properly. 46 | */ 47 | class CardTypeTest { 48 | 49 | @ParameterizedTest(name = "CardStatus.{0} prefixes cannot be modified at run time") 50 | @EnumSource(CardType.class) 51 | void cardTypeEnumIsUnmodifiable_prefixes() { 52 | assertThrows(UnsupportedOperationException.class, () -> Arrays.stream(CardType.values()).forEach(cardType -> cardType.getLengths().clear())); 53 | } 54 | 55 | @ParameterizedTest(name = "CardStatus.{0} lengths cannot be modified at run time") 56 | @EnumSource(CardType.class) 57 | void cardTypeEnumIsUnmodifiable_lengths() { 58 | assertThrows(UnsupportedOperationException.class, () -> Arrays.stream(CardType.values()).forEach(cardType -> cardType.getLengths().clear())); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/org/loverde/paymentcard/PaymentCardGeneratorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PaymentCardGenerator 3 | * https://www.github.com/kloverde/java-PaymentCardGenerator 4 | * 5 | * Copyright (c) 2016 Kurtis LoVerde 6 | * All rights reserved 7 | * 8 | * Donations: https://paypal.me/KurtisLoVerde/5 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions are met: 12 | * 13 | * 1. Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 2. Redistributions in binary form must reproduce the above copyright 16 | * notice, this list of conditions and the following disclaimer in the 17 | * documentation and/or other materials provided with the distribution. 18 | * 3. Neither the name of the copyright holder nor the names of its 19 | * contributors may be used to endorse or promote products derived from 20 | * this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | package org.loverde.paymentcard; 35 | 36 | import org.junit.jupiter.api.Test; 37 | 38 | import java.util.HashSet; 39 | import java.util.List; 40 | import java.util.Map; 41 | import java.util.Map.Entry; 42 | import java.util.Set; 43 | 44 | import static org.junit.jupiter.api.Assertions.assertEquals; 45 | import static org.junit.jupiter.api.Assertions.assertFalse; 46 | import static org.junit.jupiter.api.Assertions.assertThrows; 47 | import static org.junit.jupiter.api.Assertions.assertTrue; 48 | 49 | 50 | class PaymentCardGeneratorTest { 51 | 52 | private static final int HOW_MANY_OF_EACH = 30; 53 | 54 | private final PaymentCardGenerator generator = new PaymentCardGeneratorImpl(); 55 | 56 | 57 | @Test 58 | void generateByCardType_null() { 59 | assertThrows(IllegalArgumentException.class, () -> generator.generateByCardType(null)); 60 | } 61 | 62 | @Test 63 | void generateByCardType_amex() { 64 | final CardType cardType = CardType.AMERICAN_EXPRESS; 65 | final String cardNum = generator.generateByCardType(cardType); 66 | 67 | validateCardNumber(cardType, cardNum); 68 | } 69 | 70 | @Test 71 | void generateByCardType_visa() { 72 | final CardType cardType = CardType.VISA; 73 | final String cardNum = generator.generateByCardType(cardType); 74 | 75 | validateCardNumber(cardType, cardNum); 76 | } 77 | 78 | @Test 79 | void generateByCardType_mastercard() { 80 | final CardType cardType = CardType.MASTERCARD; 81 | final String cardNum = generator.generateByCardType(cardType); 82 | 83 | validateCardNumber(cardType, cardNum); 84 | } 85 | 86 | @Test 87 | void generateByCardType_discover() { 88 | final CardType cardType = CardType.DISCOVER; 89 | final String cardNum = generator.generateByCardType(cardType); 90 | 91 | validateCardNumber(cardType, cardNum); 92 | } 93 | 94 | @Test 95 | void generateListByCardType_null() { 96 | assertThrows(IllegalArgumentException.class, () -> generator.generateListByCardType(2, null)); 97 | } 98 | 99 | @Test 100 | void generateListByCardType_zero() { 101 | assertThrows(IllegalArgumentException.class, () -> generator.generateListByCardType(0, CardType.AMERICAN_EXPRESS)); 102 | } 103 | 104 | @Test 105 | void generateListByCardType_negative() { 106 | assertThrows(IllegalArgumentException.class, () -> generator.generateListByCardType(-1, CardType.DISCOVER)); 107 | } 108 | 109 | @Test 110 | void generateListByCardType_amex() { 111 | final CardType cardType = CardType.AMERICAN_EXPRESS; 112 | final List cardNums = generator.generateListByCardType(HOW_MANY_OF_EACH, cardType); 113 | 114 | generateListByCardType_validate(cardType, cardNums); 115 | } 116 | 117 | @Test 118 | void generateListByCardType_visa() { 119 | final CardType cardType = CardType.VISA; 120 | final List cardNums = generator.generateListByCardType(HOW_MANY_OF_EACH, cardType); 121 | 122 | generateListByCardType_validate(cardType, cardNums); 123 | } 124 | 125 | @Test 126 | void generateListByCardType_mastercard() { 127 | final CardType cardType = CardType.MASTERCARD; 128 | final List cardNums = generator.generateListByCardType(HOW_MANY_OF_EACH, cardType); 129 | 130 | generateListByCardType_validate(cardType, cardNums); 131 | } 132 | 133 | @Test 134 | void generateListByCardType_discover() { 135 | final CardType cardType = CardType.DISCOVER; 136 | final List cardNums = generator.generateListByCardType(HOW_MANY_OF_EACH, cardType); 137 | 138 | generateListByCardType_validate(cardType, cardNums); 139 | } 140 | 141 | private void generateListByCardType_validate(final CardType cardType, final List cards) { 142 | assertEquals(HOW_MANY_OF_EACH, cards.size()); 143 | 144 | for (final String cardNum : cards) { 145 | validateCardNumber(cardType, cardNum); 146 | } 147 | } 148 | 149 | @Test 150 | void generateMapByCardTypes_nullCardTypes() { 151 | assertThrows(IllegalArgumentException.class, () -> generator.generateMapByCardTypes(2, (CardType[]) null)); 152 | } 153 | 154 | @Test 155 | void generateMapByCardTypes_emptyCardTypes() { 156 | assertThrows(IllegalArgumentException.class, () -> generator.generateMapByCardTypes(2, new CardType[]{})); 157 | } 158 | 159 | @Test 160 | void generateMapByCardTypes_zero() { 161 | assertThrows(IllegalArgumentException.class, () -> generator.generateMapByCardTypes(0, CardType.AMERICAN_EXPRESS)); 162 | } 163 | 164 | @Test 165 | void generateMapByCardTypes_negative() { 166 | assertThrows(IllegalArgumentException.class, () -> generator.generateMapByCardTypes(-1, CardType.AMERICAN_EXPRESS)); 167 | } 168 | 169 | @Test 170 | void generateMapByCardTypes() { 171 | final Map> cardMap = generator.generateMapByCardTypes(HOW_MANY_OF_EACH, CardType.values()); 172 | 173 | assertEquals(CardType.values().length, cardMap.size(), "Didn't generate all of the specified card types"); 174 | 175 | int howManyCards = 0; 176 | 177 | for (final List list : cardMap.values()) { 178 | howManyCards += list.size(); 179 | } 180 | 181 | assertEquals(HOW_MANY_OF_EACH * CardType.values().length, howManyCards, "Didn't generate the correct number of cards"); 182 | 183 | for (final Entry> entry : cardMap.entrySet()) { 184 | final List cardNumbers = entry.getValue(); 185 | 186 | for (final String num : cardNumbers) { 187 | validateCardNumber(entry.getKey(), num); 188 | } 189 | } 190 | } 191 | 192 | @Test 193 | void generateByPrefix_makeZero() { 194 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(0, CardType.AMERICAN_EXPRESS.getLengths(), CardType.AMERICAN_EXPRESS.getPrefixes())); 195 | } 196 | 197 | @Test 198 | void generateByPrefix_makeNegative() { 199 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(-1, CardType.AMERICAN_EXPRESS.getLengths(), CardType.AMERICAN_EXPRESS.getPrefixes())); 200 | } 201 | 202 | @Test 203 | void generateByPrefix_nullLengths() { 204 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(1, null, CardType.AMERICAN_EXPRESS.getPrefixes())); 205 | } 206 | 207 | @Test 208 | void generateByPrefix_emptyLengths() { 209 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(1, Set.of(), CardType.AMERICAN_EXPRESS.getPrefixes())); 210 | } 211 | 212 | @Test 213 | void generateByPrefix_invalidLengths_zero() { 214 | final Set lengths = Set.of(0); 215 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(1, lengths, CardType.AMERICAN_EXPRESS.getPrefixes())); 216 | } 217 | 218 | @Test 219 | void generateByPrefix_invalidLengths_negative() { 220 | final Set lengths = Set.of(-1); 221 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(1, lengths, CardType.AMERICAN_EXPRESS.getPrefixes())); 222 | } 223 | 224 | @Test 225 | void generateByPrefix_invalidLengths_lessThan2() { 226 | final Set lengths = Set.of(1); 227 | 228 | // Ensure that we hit the correct validation failure: the prefix is not longer than the length 229 | final Set prefixes = new HashSet<>(); 230 | prefixes.add(1L); 231 | 232 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(1, lengths, prefixes)); 233 | } 234 | 235 | @Test 236 | void generateByPrefix_minimumLengthOf2() { 237 | final Set lengths = Set.of(2); 238 | 239 | final Set prefixes = new HashSet<>(); 240 | prefixes.add(1L); 241 | 242 | generator.generateByPrefix(1, lengths, prefixes); 243 | } 244 | 245 | @Test 246 | void generateByPrefix_nullPrefixes() { 247 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(1, CardType.AMERICAN_EXPRESS.getLengths(), null)); 248 | } 249 | 250 | @Test 251 | void generateByPrefix_emptyPrefixes() { 252 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(1, CardType.AMERICAN_EXPRESS.getLengths(), new HashSet<>())); 253 | } 254 | 255 | @Test 256 | void generateByPrefix_prefixIsLongerThanLength() { 257 | final Set lengths = Set.of(4); 258 | 259 | final Set prefixes = new HashSet<>(); 260 | prefixes.add(12345L); 261 | 262 | assertThrows(IllegalArgumentException.class, () -> generator.generateByPrefix(1, lengths, prefixes)); 263 | } 264 | 265 | @Test 266 | void generateByPrefix_amex() { 267 | final CardType cardType = CardType.AMERICAN_EXPRESS; 268 | final Map> cards = generator.generateByPrefix(HOW_MANY_OF_EACH, cardType.getLengths(), cardType.getPrefixes()); 269 | 270 | generateByPrefix_validate(HOW_MANY_OF_EACH * cardType.getPrefixes().size(), cardType.getLengths(), cardType.getPrefixes(), cards); 271 | } 272 | 273 | @Test 274 | void generateByPrefix_visa() { 275 | final CardType cardType = CardType.VISA; 276 | final Map> cards = generator.generateByPrefix(HOW_MANY_OF_EACH, cardType.getLengths(), cardType.getPrefixes()); 277 | 278 | generateByPrefix_validate(HOW_MANY_OF_EACH * cardType.getPrefixes().size(), cardType.getLengths(), cardType.getPrefixes(), cards); 279 | } 280 | 281 | @Test 282 | void generateByPrefix_mastercard() { 283 | final CardType cardType = CardType.MASTERCARD; 284 | final Map> cards = generator.generateByPrefix(HOW_MANY_OF_EACH, cardType.getLengths(), cardType.getPrefixes()); 285 | 286 | generateByPrefix_validate(HOW_MANY_OF_EACH * cardType.getPrefixes().size(), cardType.getLengths(), cardType.getPrefixes(), cards); 287 | } 288 | 289 | @Test 290 | void generateByPrefix_discover() { 291 | final CardType cardType = CardType.AMERICAN_EXPRESS; 292 | final Map> cards = generator.generateByPrefix(HOW_MANY_OF_EACH, cardType.getLengths(), cardType.getPrefixes()); 293 | 294 | generateByPrefix_validate(HOW_MANY_OF_EACH * cardType.getPrefixes().size(), cardType.getLengths(), cardType.getPrefixes(), cards); 295 | } 296 | 297 | @Test 298 | void generateByPrefix_someFutureFormat() { 299 | final Set lengths = Set.of(15, 17); 300 | final Set prefixes = new HashSet<>(); 301 | 302 | prefixes.add(987L); 303 | 304 | final Map> cards = generator.generateByPrefix(HOW_MANY_OF_EACH, lengths, prefixes); 305 | 306 | generateByPrefix_validate(HOW_MANY_OF_EACH, lengths, prefixes, cards); 307 | } 308 | 309 | private void generateByPrefix_validate(final int expectedCards, final Set validLengths, final Set validPrefixes, final Map> cardsMap) { 310 | int howManyCards = 0; 311 | 312 | for (final List list : cardsMap.values()) { 313 | howManyCards += list.size(); 314 | } 315 | 316 | assertEquals(expectedCards, howManyCards, "Didn't generate the correct number of cards"); 317 | 318 | for (final Entry> entry : cardsMap.entrySet()) { 319 | final List cardNumbers = entry.getValue(); 320 | 321 | for (final String num : cardNumbers) { 322 | assertTrue(num.startsWith(entry.getKey().toString()), "Card number " + num + " was put in the bucket for prefix " + entry.getKey()); 323 | validateCardNumber(validLengths, validPrefixes, num); 324 | } 325 | } 326 | } 327 | 328 | @Test 329 | void passesLuhnCheck() { 330 | assertTrue(generator.passesLuhnCheck("378282246310005")); 331 | assertTrue(generator.passesLuhnCheck("4111111111111111")); 332 | assertTrue(generator.passesLuhnCheck("5105105105105100")); 333 | assertTrue(generator.passesLuhnCheck("6011111111111117")); 334 | 335 | assertFalse(generator.passesLuhnCheck("378282246310004")); 336 | assertFalse(generator.passesLuhnCheck("411111111111111")); 337 | } 338 | 339 | private void validateCardNumber(final CardType cardType, final String cardNum) { 340 | validateCardNumber(cardType.getLengths(), cardType.getPrefixes(), cardNum); 341 | } 342 | 343 | private void validateCardNumber(final Set validLengths, final Set validPrefixes, final String cardNum) { 344 | assertTrue(validLengths.contains(cardNum.length()), "Card number " + cardNum + " has an invalid length"); 345 | assertTrue(generator.passesLuhnCheck(cardNum), "Card number " + cardNum + " doesn't pass Luhn validation"); 346 | 347 | boolean validPrefix = false; 348 | 349 | for (final Long prefix : validPrefixes) { 350 | if (cardNum.startsWith(prefix.toString())) { 351 | validPrefix = true; 352 | break; 353 | } 354 | } 355 | 356 | assertTrue(validPrefix, "Card number " + cardNum + " has an invalid prefix"); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/test/java/org/loverde/paymentcard/RangeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PaymentCardGenerator 3 | * https://www.github.com/kloverde/java-PaymentCardGenerator 4 | * 5 | * Copyright (c) 2016 Kurtis LoVerde 6 | * All rights reserved 7 | * 8 | * Donations: https://paypal.me/KurtisLoVerde/5 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions are met: 12 | * 13 | * 1. Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 2. Redistributions in binary form must reproduce the above copyright 16 | * notice, this list of conditions and the following disclaimer in the 17 | * documentation and/or other materials provided with the distribution. 18 | * 3. Neither the name of the copyright holder nor the names of its 19 | * contributors may be used to endorse or promote products derived from 20 | * this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | package org.loverde.paymentcard; 35 | 36 | import org.junit.jupiter.api.Test; 37 | 38 | import static org.junit.jupiter.api.Assertions.assertEquals; 39 | import static org.junit.jupiter.api.Assertions.assertThrows; 40 | 41 | 42 | class RangeTest { 43 | 44 | @Test 45 | void size_startEndSame() { 46 | assertEquals(1, new Range(100, 100).size()); 47 | } 48 | 49 | @Test 50 | void size_startEndDifferent() { 51 | assertEquals(3, new Range(100, 102).size()); 52 | } 53 | 54 | @Test 55 | void size_startGreaterThanEnd() { 56 | assertThrows(IllegalArgumentException.class, () -> new Range(2, 1)); 57 | } 58 | } 59 | --------------------------------------------------------------------------------