29-credit-card-number-checker (#81)

# Describe Request

Credit card checker added.

Fixes #29

# Change Type

New checker.
This commit is contained in:
Onur Cinar 2023-06-21 12:36:54 -07:00 committed by GitHub
commit 26c9fd6ea9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 453 additions and 0 deletions

View file

@ -73,6 +73,7 @@ This package currently provides the following checkers:
- [alphanumeric](doc/checkers/alphanumeric.md) checks if the given string consists of only alphanumeric characters.
- [ascii](doc/checkers/ascii.md) checks if the given string consists of only ASCII characters.
- [cidr](doc/checkers/cidr.md) checker checks if the value is a valid CIDR notation IP address and prefix length.
- [credit-card](doc/checkers/credit_card.md) checks if the given value is a valid credit card number.
- [digits](doc/checkers/digits.md) checks if the given string consists of only digit characters.
- [email](doc/checkers/email.md) checks if the given string is an email address.
- [fqdn](doc/checkers/fqdn.md) checks if the given string is a fully qualified domain name.

View file

@ -33,6 +33,7 @@ const ResultValid Result = "VALID"
var makers = map[string]MakeFunc{
CheckerAlphanumeric: makeAlphanumeric,
CheckerASCII: makeASCII,
CheckerCreditCard: makeCreditCard,
CheckerCidr: makeCidr,
CheckerDigits: makeDigits,
CheckerEmail: makeEmail,

146
credit_card.go Normal file
View file

@ -0,0 +1,146 @@
package checker
import (
"reflect"
"regexp"
"strings"
)
// CheckerCreditCard is the name of the checker.
const CheckerCreditCard = "credit-card"
// ResultNotCreditCard indicates that the given value is not a valid credit card number.
const ResultNotCreditCard = "NOT_CREDIT_CARD"
// amexExpression is the regexp for the AMEX cards. They start with 34 or 37, and has 15 digits.
var amexExpression = "(?:^(?:3[47])[0-9]{13}$)"
var amexPattern = regexp.MustCompile(amexExpression)
// dinersExpression is the regexp for the Diners cards. They start with 305, 36, 38, and has 14 digits.
var dinersExpression = "(?:^3(?:(?:05[0-9]{11})|(?:[68][0-9]{12}))$)"
var dinersPattern = regexp.MustCompile(dinersExpression)
// discoverExpression is the regexp for the Discover cards. They start with 6011 and has 16 digits.
var discoverExpression = "(?:^6011[0-9]{12}$)"
var discoverPattern = regexp.MustCompile(discoverExpression)
// jcbExpression is the regexp for the JCB 15 cards. They start with 2131, 1800, and has 15 digits, or start with 35 and has 16 digits.
var jcbExpression = "(?:^(?:(?:2131)|(?:1800)|(?:35[0-9]{3}))[0-9]{11}$)"
var jcbPattern = regexp.MustCompile(jcbExpression)
// masterCardExpression is the regexp for the MasterCard cards. They start with 51, 52, 53, 54, or 55, and has 15 digits.
var masterCardExpression = "(?:^5[12345][0-9]{14}$)"
var masterCardPattern = regexp.MustCompile(masterCardExpression)
// unionPayExpression is the regexp for the UnionPay cards. They start either with 62 or 67, and has 16 digits, or they start with 81 and has 16 to 19 digits.
var unionPayExpression = "(?:(?:6[27][0-9]{14})|(?:81[0-9]{14,17})^$)"
var unionPayPattern = regexp.MustCompile(unionPayExpression)
// visaExpression is the regexp for the Visa cards. They start with 4 and has 13 or 16 digits.
var visaExpression = "(?:^4[0-9]{12}(?:[0-9]{3})?$)"
var visaPattern = regexp.MustCompile(visaExpression)
// anyCreditCardPattern is the regexp for any credit card.
var anyCreditCardPattern = regexp.MustCompile(strings.Join([]string{
amexExpression,
dinersExpression,
discoverExpression,
jcbExpression,
masterCardExpression,
unionPayExpression,
visaExpression,
}, "|"))
// creditCardPatterns is the mapping for credit card names to patterns.
var creditCardPatterns = map[string]*regexp.Regexp{
"amex": amexPattern,
"diners": dinersPattern,
"discover": discoverPattern,
"jcb": jcbPattern,
"mastercard": masterCardPattern,
"unionpay": unionPayPattern,
"visa": visaPattern,
}
// IsAnyCreditCard checks if the given value is a valid credit card number.
func IsAnyCreditCard(number string) Result {
return isCreditCard(number, anyCreditCardPattern)
}
// IsAmexCreditCard checks if the given valie is a valid AMEX credit card.
func IsAmexCreditCard(number string) Result {
return isCreditCard(number, amexPattern)
}
// IsDinersCreditCard checks if the given valie is a valid Diners credit card.
func IsDinersCreditCard(number string) Result {
return isCreditCard(number, dinersPattern)
}
// IsDiscoveryCreditCard checks if the given valie is a valid Discovery credit card.
func IsDiscoveryCreditCard(number string) Result {
return isCreditCard(number, discoverPattern)
}
// IsJcbCreditCard checks if the given valie is a valid JCB 15 credit card.
func IsJcbCreditCard(number string) Result {
return isCreditCard(number, jcbPattern)
}
// IsMasterCardCreditCard checks if the given valie is a valid MasterCard credit card.
func IsMasterCardCreditCard(number string) Result {
return isCreditCard(number, masterCardPattern)
}
// IsUnionPayCreditCard checks if the given valie is a valid UnionPay credit card.
func IsUnionPayCreditCard(number string) Result {
return isCreditCard(number, unionPayPattern)
}
// IsVisaCreditCard checks if the given valie is a valid Visa credit card.
func IsVisaCreditCard(number string) Result {
return isCreditCard(number, visaPattern)
}
// makeCreditCard makes a checker function for the credit card checker.
func makeCreditCard(config string) CheckFunc {
patterns := []*regexp.Regexp{}
if config != "" {
for _, card := range strings.Split(config, ",") {
pattern, ok := creditCardPatterns[card]
if !ok {
panic("unknown credit card name")
}
patterns = append(patterns, pattern)
}
} else {
patterns = append(patterns, anyCreditCardPattern)
}
return func(value, _ reflect.Value) Result {
if value.Kind() != reflect.String {
panic("string expected")
}
number := value.String()
for _, pattern := range patterns {
if isCreditCard(number, pattern) == ResultValid {
return ResultValid
}
}
return ResultNotCreditCard
}
}
// isCreditCard checks if the given number based on the given credit card pattern and the Luhn algorithm check digit.
func isCreditCard(number string, pattern *regexp.Regexp) Result {
if !pattern.MatchString(number) {
return ResultNotCreditCard
}
return IsLuhn(number)
}

243
credit_card_test.go Normal file
View file

@ -0,0 +1,243 @@
package checker
import (
"strconv"
"testing"
)
// Test numbers from https://stripe.com/docs/testing
var invalidCard = "1234123412341234"
var amexCard = "378282246310005"
var dinersCard = "36227206271667"
var discoverCard = "6011111111111117"
var jcbCard = "3530111333300000"
var masterCard = "5555555555554444"
var unionPayCard = "6200000000000005"
var visaCard = "4111111111111111"
// changeToInvalidLuhn increments the luhn digit to make the number invalid. It assumes that the given number is valid.
func changeToInvalidLuhn(number string) string {
luhn, err := strconv.Atoi(number[len(number)-1:])
if err != nil {
panic(err)
}
luhn = (luhn + 1) % 10
return number[:len(number)-1] + strconv.Itoa(luhn)
}
func TestIsAnyCreditCardValid(t *testing.T) {
if IsAnyCreditCard(amexCard) != ResultValid {
t.Fail()
}
}
func TestIsAnyCreditCardInvalidPattern(t *testing.T) {
if IsAnyCreditCard(invalidCard) == ResultValid {
t.Fail()
}
}
func TestIsAnyCreditCardInvalidLuhn(t *testing.T) {
if IsAnyCreditCard(changeToInvalidLuhn(amexCard)) == ResultValid {
t.Fail()
}
}
func TestIsAmexCreditCardValid(t *testing.T) {
if IsAmexCreditCard(amexCard) != ResultValid {
t.Fail()
}
}
func TestIsAmexCreditCardInvalidPattern(t *testing.T) {
if IsAmexCreditCard(invalidCard) == ResultValid {
t.Fail()
}
}
func TestIsAmexCreditCardInvalidLuhn(t *testing.T) {
if IsAmexCreditCard(changeToInvalidLuhn(amexCard)) == ResultValid {
t.Fail()
}
}
func TestIsDinersCreditCardValid(t *testing.T) {
if IsDinersCreditCard(dinersCard) != ResultValid {
t.Fail()
}
}
func TestIsDinersCreditCardInvalidPattern(t *testing.T) {
if IsDinersCreditCard(invalidCard) == ResultValid {
t.Fail()
}
}
func TestIsDinersCreditCardInvalidLuhn(t *testing.T) {
if IsDinersCreditCard(changeToInvalidLuhn(dinersCard)) == ResultValid {
t.Fail()
}
}
func TestIsDiscoverCreditCardValid(t *testing.T) {
if IsDiscoveryCreditCard(discoverCard) != ResultValid {
t.Fail()
}
}
func TestIsDiscoverCreditCardInvalidPattern(t *testing.T) {
if IsDiscoveryCreditCard(invalidCard) == ResultValid {
t.Fail()
}
}
func TestIsDiscoverCreditCardInvalidLuhn(t *testing.T) {
if IsDiscoveryCreditCard(changeToInvalidLuhn(discoverCard)) == ResultValid {
t.Fail()
}
}
func TestIsJcbCreditCardValid(t *testing.T) {
if IsJcbCreditCard(jcbCard) != ResultValid {
t.Fail()
}
}
func TestIsJcbCreditCardInvalidPattern(t *testing.T) {
if IsJcbCreditCard(invalidCard) == ResultValid {
t.Fail()
}
}
func TestIsJcbCreditCardInvalidLuhn(t *testing.T) {
if IsJcbCreditCard(changeToInvalidLuhn(jcbCard)) == ResultValid {
t.Fail()
}
}
func TestIsMasterCardCreditCardValid(t *testing.T) {
if IsMasterCardCreditCard(masterCard) != ResultValid {
t.Fail()
}
}
func TestIsMasterCardCreditCardInvalidPattern(t *testing.T) {
if IsMasterCardCreditCard(invalidCard) == ResultValid {
t.Fail()
}
}
func TestIsMasterCardCreditCardInvalidLuhn(t *testing.T) {
if IsMasterCardCreditCard(changeToInvalidLuhn(masterCard)) == ResultValid {
t.Fail()
}
}
func TestIsUnionPayCreditCardValid(t *testing.T) {
if IsUnionPayCreditCard(unionPayCard) != ResultValid {
t.Fail()
}
}
func TestIsUnionPayCreditCardInvalidPattern(t *testing.T) {
if IsUnionPayCreditCard(invalidCard) == ResultValid {
t.Fail()
}
}
func TestIsUnionPayCreditCardInvalidLuhn(t *testing.T) {
if IsUnionPayCreditCard(changeToInvalidLuhn(unionPayCard)) == ResultValid {
t.Fail()
}
}
func TestIsVisaCreditCardValid(t *testing.T) {
if IsVisaCreditCard(visaCard) != ResultValid {
t.Fail()
}
}
func TestIsVisaCreditCardInvalidPattern(t *testing.T) {
if IsVisaCreditCard(invalidCard) == ResultValid {
t.Fail()
}
}
func TestIsVisaCreditCardInvalidLuhn(t *testing.T) {
if IsVisaCreditCard(changeToInvalidLuhn(visaCard)) == ResultValid {
t.Fail()
}
}
func TestCheckCreditCardNonString(t *testing.T) {
defer FailIfNoPanic(t)
type Order struct {
CreditCard int `checkers:"credit-card"`
}
order := &Order{}
Check(order)
}
func TestCheckCreditCardValid(t *testing.T) {
type Order struct {
CreditCard string `checkers:"credit-card"`
}
order := &Order{
CreditCard: amexCard,
}
_, valid := Check(order)
if !valid {
t.Fail()
}
}
func TestCheckCreditCardInvalid(t *testing.T) {
type Order struct {
CreditCard string `checkers:"credit-card"`
}
order := &Order{
CreditCard: invalidCard,
}
_, valid := Check(order)
if valid {
t.Fail()
}
}
func TestCheckCreditCardMultipleUnknown(t *testing.T) {
defer FailIfNoPanic(t)
type Order struct {
CreditCard string `checkers:"credit-card:amex,unknown"`
}
order := &Order{
CreditCard: amexCard,
}
Check(order)
}
func TestCheckCreditCardMultipleInvalid(t *testing.T) {
type Order struct {
CreditCard string `checkers:"credit-card:amex,visa"`
}
order := &Order{
CreditCard: discoverCard,
}
_, valid := Check(order)
if valid {
t.Fail()
}
}

View file

@ -0,0 +1,62 @@
# Credit Card Checker
The ```credit-card``` checker checks if the given value is a valid credit card number. If the given value is not a valid credit card number, the checker will return the ```NOT_CREDIT_CARD``` result.
Here is an example:
```golang
type Order struct {
CreditCard string `checkers:"credit-card"`
}
order := &Order{
CreditCard: invalidCard,
}
_, valid := Check(order)
if valid {
// Send the mistakes back to the user
}
```
The checker currently knows about AMEX, Diners, Discover, JCB, MasterCard, UnionPay, and VISA credit card numbers.
If you would like to check for a subset of those credit cards, you can specify them through the checker config parameter. Here is an example:
```golang
type Order struct {
CreditCard string `checkers:"credit-card:amex,visa"`
}
order := &Order{
CreditCard: "6011111111111117",
}
_, valid := Check(order)
if valid {
// Send the mistakes back to the user
}
```
If you would like to verify a credit card that is not listed here, please use the [luhn](luhn.md) checker to use the Luhn Algorithm to verify the check digit.
In your custom checkers, you can call the ```credit-card``` checker functions below to validate the user input.
- IsAnyCreditCard: checks if the given value is a valid credit card number.
- IsAmexCreditCard: checks if the given value is a valid AMEX credit card number.
- IsDinersCreditCard: checks if the given value is a valid Diners credit card number.
- IsDiscoverCreditCard: checks if the given value is a valid Discover credit card number.
- IsJcbCreditCard: checks if the given value is a valid JCB credit card number.
- IsMasterCardCreditCard: checks if the given value is a valid MasterCard credit card number.
- IsUnionPayCreditCard: checks if the given value is a valid UnionPay credit card number.
- IsVisaCreditCard: checks if the given value is a valid VISA credit card number.
Here is an example:
```golang
result := IsAnyCreditCard("6011111111111117")
if result != ResultValid {
// Send the mistakes back to the user
}
```