Skip to content

Commit

Permalink
Add automated fuzz testing via a csv list of bins
Browse files Browse the repository at this point in the history
  • Loading branch information
bendrucker committed Dec 19, 2017
1 parent 69ffa19 commit efa25af
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 2 deletions.
76 changes: 76 additions & 0 deletions binlist-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict'

const test = require('tape')
const get = require('simple-get')
const csv = require('csv-parser')
const pump = require('pump')
const {Transform} = require('stream')
const array = require('cast-array')
const randomInt = require('random-int')
const luhn = require('luhn-generator')

const RANGES = 'https://raw.githubusercontent.com/binlist/data/master/ranges.csv'

const ccTypes = {
discover: [
require('./types/discover'),
require('./types/maestro')
],
amex: require('./types/american-express'),
unionpay: require('./types/unionpay'),
mastercard: [
require('./types/mastercard'),
require('./types/maestro')
],
visa: require('./types/visa'),
diners: require('./types/diners-club')
}

test('binlist', function (t) {
get(RANGES, function (err, res) {
if (err) return t.end(err)
if (res.statusCode !== 200) {
return t.end(new Error('Exited with ' + res.statusCode))
}

pump(res, csv(), verifyCard(t), t.end)
})
})

function verifyCard (t) {
return new Transform({
objectMode: true,
transform: function (row, enc, callback) {
const types = ccTypes[row.scheme] && array(ccTypes[row.scheme])
if (types) testCard(types, row)
callback()
}
})

function testCard (types, range) {
['start', 'end'].forEach(function (bound) {
const value = range['iin_' + bound]
if (!value) return

const output = [range.scheme, bound, value]

t.ok(types.some(type => type.test(value, true)), ['eager'].concat(output).join(' | '))

const type = types.find(type => type.test(value, true))
const generated = generateCard(value, type)

t.ok(type.test(generated), ['strict'].concat(output, generated + ' (generated)').join(' | '))
})
}
}

function generateCard (seed, type) {
seed = String(seed)
const length = Array.isArray(type.digits) ? type.digits[1] : type.digits
const random = new Array(length - seed.length - 1)
.fill()
.map(() => randomInt(0, 9))
.join('')

return luhn.generate(seed + random)
}
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Card type definitions and methods for creditcards",
"main": "index.js",
"scripts": {
"test": "standard && tape test/*.js | tap-spec"
"test": "standard && tape test/*.js | tap-spec && tape binlist-test.js"
},
"repository": {
"type": "git",
Expand All @@ -25,7 +25,13 @@
"xtend": "~4.0.0"
},
"devDependencies": {
"cast-array": "~1.0.1",
"csv-parser": "~1.12.0",
"luhn-generator": "~0.5.1",
"pump": "~2.0.0",
"random-int": "~1.0.0",
"run-parallel": "~1.1.6",
"simple-get": "~2.7.0",
"standard": "^10.0.3",
"tap-spec": "^4.0.0",
"tape": "^4.0.0"
Expand Down
24 changes: 23 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# creditcards-types [![Build Status](https://travis-ci.org/bendrucker/creditcards-types.svg?branch=master)](https://travis-ci.org/bendrucker/creditcards-types)

> Card type definitions and methods used by [creditcards](https://github.com/bendrucker/creditcards), a JS library for all platforms for parsing, validating, and formatting credit card data.
> Card type definitions in JavaScript modules
This library powers [creditcards](https://github.com/bendrucker/creditcards), a higher level tool for parsing/formatting/validating card data. This repository focuses on [tests](#tests) and [documentation](docs). Card types are primarily represented by static values and regular expressions.

## Card Types

Expand Down Expand Up @@ -136,6 +138,26 @@ Type: `string`

The card number to group. This may be a complete or partial card number. Any digits past the type's maximum length will be discarded.

## Tests

This repository is designed to support a large volume of automated testing. There are two types of tests.

#### Regression tests (~100)

Traditional regression tests are included in the [`test/`](test) folder. These tests describe the regular expressions and make assertions about a few possible card patterns. Each type tests checks that expedcted eager matches for that type do not also match another card type. There's also a coverage check that will fail the test run if any type module is missing an identically named test file.

#### Fuzz tests (~12,500)

As an additional check, `npm test` downloads [range data from binlist](https://github.com/binlist/data). The [binlist tests](binlist-test.js):

* Check the BIN range start and end to make sure they are an eager match for their corresponding type
* Generate a random card number matching the maximum length for that type
* Strictly test the generated number against the type

This data is not guaranteed to be accurate but provides a valuable external check against the validity of the type definitions with far more assertions than could ever be written by hand.



## License

MIT © [Ben Drucker](http://bendrucker.me)
1 change: 1 addition & 0 deletions type.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ function CardType (data) {
assign(this, data)
}

CardType.prototype.digits = 16
CardType.prototype.cvcLength = 3
CardType.prototype.luhn = true
CardType.prototype.groupPattern = /(\d{1,4})(\d{1,4})?(\d{1,4})?(\d{1,4})?/
Expand Down
1 change: 1 addition & 0 deletions types/american-express.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var Type = require('../type')

module.exports = Type({
name: 'American Express',
digits: 15,
pattern: /^3[47]\d{13}$/,
eagerPattern: /^3[47]/,
groupPattern: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
Expand Down
1 change: 1 addition & 0 deletions types/diners-club.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var Type = require('../type')

module.exports = Type({
name: 'Diners Club',
digits: 14,
pattern: /^3(0[0-5]|[68]\d)\d{11}$/,
eagerPattern: /^3(0|[68])/,
groupPattern: /(\d{1,4})?(\d{1,6})?(\d{1,4})?/
Expand Down
1 change: 1 addition & 0 deletions types/maestro.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var Type = require('../type')

module.exports = Type({
name: 'Maestro',
digits: [12, 19],
pattern: /^(?:5[06789]\d\d|(?!6011[0234])(?!60117[4789])(?!60118[6789])(?!60119)(?!64[456789])(?!65)6\d{3})\d{8,15}$/,
eagerPattern: /^(5(018|0[23]|[68])|6[37]|60111|60115|60117([56]|7[56])|60118[0-5]|64[0-3]|66)/,
groupPattern: /(\d{1,4})(\d{1,4})?(\d{1,4})?(\d{1,4})?(\d{1,3})?/
Expand Down
1 change: 1 addition & 0 deletions types/visa.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var Type = require('../type')

module.exports = Type({
name: 'Visa',
digits: [13, 19],
pattern: /^4\d{12}(\d{3}|\d{6})?$/,
eagerPattern: /^4/,
groupPattern: /(\d{1,4})(\d{1,4})?(\d{1,4})?(\d{1,4})?(\d{1,3})?/
Expand Down

0 comments on commit efa25af

Please sign in to comment.