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:
parent
b709836ded
commit
48d005ce71
5 changed files with 473 additions and 4 deletions
156
v2/credit_card.go
Normal file
156
v2/credit_card.go
Normal 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
312
v2/credit_card_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ var makers = map[string]MakeCheckFunc{
|
|||
nameAlphanumeric: makeAlphanumeric,
|
||||
nameASCII: makeASCII,
|
||||
nameCIDR: makeCIDR,
|
||||
nameCreditCard: makeCreditCard,
|
||||
nameDigits: makeDigits,
|
||||
nameEmail: makeEmail,
|
||||
nameFQDN: makeFQDN,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue