Add Credit Card validation checker and tests to v2. (#141)

# Describe Request

Add Credit Card validation checker and tests to v2.

# Change Type

New code.
This commit is contained in:
Onur Cinar 2024-12-27 06:14:17 -08:00 committed by GitHub
commit 48d005ce71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 473 additions and 4 deletions

156
v2/credit_card.go Normal file
View file

@ -0,0 +1,156 @@
// Copyright (c) 2023-2024 Onur Cinar.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://github.com/cinar/checker
package v2
import (
"reflect"
"regexp"
"strings"
)
const (
// nameCreditCard is the name of the credit card check.
nameCreditCard = "credit-card"
)
var (
// ErrNotCreditCard indicates that the given value is not a valid credit card number.
ErrNotCreditCard = NewCheckError("CreditCard")
// amexExpression is the regexp for the AMEX cards. They start with 34 or 37, and has 15 digits.
amexExpression = "(?:^(?:3[47])[0-9]{13}$)"
amexPattern = regexp.MustCompile(amexExpression)
// dinersExpression is the regexp for the Diners cards. They start with 305, 36, 38, and has 14 digits.
dinersExpression = "(?:^3(?:(?:05[0-9]{11})|(?:[68][0-9]{12}))$)"
dinersPattern = regexp.MustCompile(dinersExpression)
// discoverExpression is the regexp for the Discover cards. They start with 6011 and has 16 digits.
discoverExpression = "(?:^6011[0-9]{12}$)"
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.
jcbExpression = "(?:^(?:(?:2131)|(?:1800)|(?:35[0-9]{3}))[0-9]{11}$)"
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.
masterCardExpression = "(?:^5[12345][0-9]{14}$)"
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.
unionPayExpression = "(?:(?:6[27][0-9]{14})|(?:81[0-9]{14,17})^$)"
unionPayPattern = regexp.MustCompile(unionPayExpression)
// visaExpression is the regexp for the Visa cards. They start with 4 and has 13 or 16 digits.
visaExpression = "(?:^4[0-9]{12}(?:[0-9]{3})?$)"
visaPattern = regexp.MustCompile(visaExpression)
// anyCreditCardPattern is the regexp for any credit card.
anyCreditCardPattern = regexp.MustCompile(strings.Join([]string{
amexExpression,
dinersExpression,
discoverExpression,
jcbExpression,
masterCardExpression,
unionPayExpression,
visaExpression,
}, "|"))
// creditCardPatterns is the mapping for credit card names to patterns.
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) (string, error) {
return isCreditCard(number, anyCreditCardPattern)
}
// IsAmexCreditCard checks if the given valie is a valid AMEX credit card.
func IsAmexCreditCard(number string) (string, error) {
return isCreditCard(number, amexPattern)
}
// IsDinersCreditCard checks if the given valie is a valid Diners credit card.
func IsDinersCreditCard(number string) (string, error) {
return isCreditCard(number, dinersPattern)
}
// IsDiscoverCreditCard checks if the given valie is a valid Discover credit card.
func IsDiscoverCreditCard(number string) (string, error) {
return isCreditCard(number, discoverPattern)
}
// IsJcbCreditCard checks if the given valie is a valid JCB 15 credit card.
func IsJcbCreditCard(number string) (string, error) {
return isCreditCard(number, jcbPattern)
}
// IsMasterCardCreditCard checks if the given valie is a valid MasterCard credit card.
func IsMasterCardCreditCard(number string) (string, error) {
return isCreditCard(number, masterCardPattern)
}
// IsUnionPayCreditCard checks if the given valie is a valid UnionPay credit card.
func IsUnionPayCreditCard(number string) (string, error) {
return isCreditCard(number, unionPayPattern)
}
// IsVisaCreditCard checks if the given valie is a valid Visa credit card.
func IsVisaCreditCard(number string) (string, error) {
return isCreditCard(number, visaPattern)
}
// makeCreditCard makes a checker function for the credit card checker.
func makeCreditCard(config string) CheckFunc[reflect.Value] {
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) (reflect.Value, error) {
if value.Kind() != reflect.String {
panic("string expected")
}
number := value.String()
for _, pattern := range patterns {
_, err := isCreditCard(number, pattern)
if err == nil {
return value, nil
}
}
return value, ErrNotCreditCard
}
}
// 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) (string, error) {
if !pattern.MatchString(number) {
return number, ErrNotCreditCard
}
return IsLUHN(number)
}

312
v2/credit_card_test.go Normal file
View file

@ -0,0 +1,312 @@
// Copyright (c) 2023-2024 Onur Cinar.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://github.com/cinar/checker
package v2_test
import (
"strconv"
"testing"
v2 "github.com/cinar/checker/v2"
)
// 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 ExampleIsAnyCreditCard() {
_, err := v2.IsAnyCreditCard("6011111111111117")
if err != nil {
// Send the errors back to the user
}
}
func TestIsAnyCreditCardValid(t *testing.T) {
_, err := v2.IsAnyCreditCard(amexCard)
if err != nil {
t.Error(err)
}
}
func TestIsAnyCreditCardInvalidPattern(t *testing.T) {
if _, err := v2.IsAnyCreditCard(invalidCard); err == nil {
t.Error("expected error for invalid card pattern")
}
}
func TestIsAnyCreditCardInvalidLuhn(t *testing.T) {
if _, err := v2.IsAnyCreditCard(changeToInvalidLuhn(amexCard)); err == nil {
t.Error("expected error for invalid Luhn")
}
}
func ExampleIsAmexCreditCard() {
_, err := v2.IsAmexCreditCard("378282246310005")
if err != nil {
// Send the errors back to the user
}
}
func TestIsAmexCreditCardValid(t *testing.T) {
if _, err := v2.IsAmexCreditCard(amexCard); err != nil {
t.Error(err)
}
}
func TestIsAmexCreditCardInvalidPattern(t *testing.T) {
if _, err := v2.IsAmexCreditCard(invalidCard); err == nil {
t.Error("expected error for invalid card pattern")
}
}
func TestIsAmexCreditCardInvalidLuhn(t *testing.T) {
if _, err := v2.IsAmexCreditCard(changeToInvalidLuhn(amexCard)); err == nil {
t.Error("expected error for invalid Luhn")
}
}
func ExampleIsDinersCreditCard() {
_, err := v2.IsDinersCreditCard("36227206271667")
if err != nil {
// Send the errors back to the user
}
}
func TestIsDinersCreditCardValid(t *testing.T) {
if _, err := v2.IsDinersCreditCard(dinersCard); err != nil {
t.Error(err)
}
}
func TestIsDinersCreditCardInvalidPattern(t *testing.T) {
if _, err := v2.IsDinersCreditCard(invalidCard); err == nil {
t.Error("expected error for invalid card pattern")
}
}
func TestIsDinersCreditCardInvalidLuhn(t *testing.T) {
if _, err := v2.IsDinersCreditCard(changeToInvalidLuhn(dinersCard)); err == nil {
t.Error("expected error for invalid Luhn")
}
}
func ExampleIsDiscoverCreditCard() {
_, err := v2.IsDiscoverCreditCard("6011111111111117")
if err != nil {
// Send the errors back to the user
}
}
func TestIsDiscoverCreditCardValid(t *testing.T) {
if _, err := v2.IsDiscoverCreditCard(discoverCard); err != nil {
t.Error(err)
}
}
func TestIsDiscoverCreditCardInvalidPattern(t *testing.T) {
if _, err := v2.IsDiscoverCreditCard(invalidCard); err == nil {
t.Error("expected error for invalid card pattern")
}
}
func TestIsDiscoverCreditCardInvalidLuhn(t *testing.T) {
if _, err := v2.IsDiscoverCreditCard(changeToInvalidLuhn(discoverCard)); err == nil {
t.Error("expected error for invalid Luhn")
}
}
func ExampleIsJcbCreditCard() {
_, err := v2.IsJcbCreditCard("3530111333300000")
if err != nil {
// Send the errors back to the user
}
}
func TestIsJcbCreditCardValid(t *testing.T) {
if _, err := v2.IsJcbCreditCard(jcbCard); err != nil {
t.Error(err)
}
}
func TestIsJcbCreditCardInvalidPattern(t *testing.T) {
if _, err := v2.IsJcbCreditCard(invalidCard); err == nil {
t.Error("expected error for invalid card pattern")
}
}
func TestIsJcbCreditCardInvalidLuhn(t *testing.T) {
if _, err := v2.IsJcbCreditCard(changeToInvalidLuhn(jcbCard)); err == nil {
t.Error("expected error for invalid Luhn")
}
}
func ExampleIsMasterCardCreditCard() {
_, err := v2.IsMasterCardCreditCard("5555555555554444")
if err != nil {
// Send the errors back to the user
}
}
func TestIsMasterCardCreditCardValid(t *testing.T) {
if _, err := v2.IsMasterCardCreditCard(masterCard); err != nil {
t.Error(err)
}
}
func TestIsMasterCardCreditCardInvalidPattern(t *testing.T) {
if _, err := v2.IsMasterCardCreditCard(invalidCard); err == nil {
t.Error("expected error for invalid card pattern")
}
}
func TestIsMasterCardCreditCardInvalidLuhn(t *testing.T) {
if _, err := v2.IsMasterCardCreditCard(changeToInvalidLuhn(masterCard)); err == nil {
t.Error("expected error for invalid Luhn")
}
}
func ExampleIsUnionPayCreditCard() {
_, err := v2.IsUnionPayCreditCard("6200000000000005")
if err != nil {
// Send the errors back to the user
}
}
func TestIsUnionPayCreditCardValid(t *testing.T) {
if _, err := v2.IsUnionPayCreditCard(unionPayCard); err != nil {
t.Error(err)
}
}
func TestIsUnionPayCreditCardInvalidPattern(t *testing.T) {
if _, err := v2.IsUnionPayCreditCard(invalidCard); err == nil {
t.Error("expected error for invalid card pattern")
}
}
func TestIsUnionPayCreditCardInvalidLuhn(t *testing.T) {
if _, err := v2.IsUnionPayCreditCard(changeToInvalidLuhn(unionPayCard)); err == nil {
t.Error("expected error for invalid Luhn")
}
}
func ExampleIsVisaCreditCard() {
_, err := v2.IsVisaCreditCard("4111111111111111")
if err != nil {
// Send the errors back to the user
}
}
func TestIsVisaCreditCardValid(t *testing.T) {
if _, err := v2.IsVisaCreditCard(visaCard); err != nil {
t.Error(err)
}
}
func TestIsVisaCreditCardInvalidPattern(t *testing.T) {
if _, err := v2.IsVisaCreditCard(invalidCard); err == nil {
t.Error("expected error for invalid card pattern")
}
}
func TestIsVisaCreditCardInvalidLuhn(t *testing.T) {
if _, err := v2.IsVisaCreditCard(changeToInvalidLuhn(visaCard)); err == nil {
t.Error("expected error for invalid Luhn")
}
}
func TestCheckCreditCardNonString(t *testing.T) {
defer FailIfNoPanic(t, "expected panic for non-string credit card")
type Order struct {
CreditCard int `checkers:"credit-card"`
}
order := &Order{}
v2.CheckStruct(order)
}
func TestCheckCreditCardValid(t *testing.T) {
type Order struct {
CreditCard string `checkers:"credit-card"`
}
order := &Order{
CreditCard: amexCard,
}
_, valid := v2.CheckStruct(order)
if !valid {
t.Fail()
}
}
func TestCheckCreditCardInvalid(t *testing.T) {
type Order struct {
CreditCard string `checkers:"credit-card"`
}
order := &Order{
CreditCard: invalidCard,
}
_, valid := v2.CheckStruct(order)
if valid {
t.Fail()
}
}
func TestCheckCreditCardMultipleUnknown(t *testing.T) {
defer FailIfNoPanic(t, "expected panic for unknown credit card")
type Order struct {
CreditCard string `checkers:"credit-card:amex,unknown"`
}
order := &Order{
CreditCard: amexCard,
}
v2.CheckStruct(order)
}
func TestCheckCreditCardMultipleInvalid(t *testing.T) {
type Order struct {
CreditCard string `checkers:"credit-card:amex,visa"`
}
order := &Order{
CreditCard: discoverCard,
}
_, valid := v2.CheckStruct(order)
if valid {
t.Fail()
}
}

View file

@ -13,7 +13,7 @@ import (
)
func ExampleIsISBN() {
_, err := v2.IsISBN("9783161484100")
_, err := v2.IsISBN("1430248270")
if err != nil {
fmt.Println(err)
}
@ -27,7 +27,7 @@ func TestIsISBNInvalid(t *testing.T) {
}
func TestIsISBNValid(t *testing.T) {
_, err := v2.IsISBN("9783161484100")
_, err := v2.IsISBN("1430248270")
if err != nil {
t.Fatal(err)
}

View file

@ -13,7 +13,7 @@ import (
)
func ExampleIsLUHN() {
_, err := v2.IsLUHN("79927398713")
_, err := v2.IsLUHN("4012888888881881")
if err != nil {
fmt.Println(err)
}
@ -27,7 +27,7 @@ func TestIsLUHNInvalid(t *testing.T) {
}
func TestIsLUHNValid(t *testing.T) {
_, err := v2.IsLUHN("79927398713")
_, err := v2.IsLUHN("4012888888881881")
if err != nil {
t.Fatal(err)
}

View file

@ -19,6 +19,7 @@ var makers = map[string]MakeCheckFunc{
nameAlphanumeric: makeAlphanumeric,
nameASCII: makeASCII,
nameCIDR: makeCIDR,
nameCreditCard: makeCreditCard,
nameDigits: makeDigits,
nameEmail: makeEmail,
nameFQDN: makeFQDN,