From c911c77654d6ec7a49bc4399b9b03d2c1cfb1091 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 16:21:41 +0100 Subject: [PATCH 01/18] added credit card number validation support --- README.md | 4 ++ SwiftValidator/Rules/CardNumberRule.swift | 48 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 SwiftValidator/Rules/CardNumberRule.swift diff --git a/README.md b/README.md index 79e3c7b..ca29b28 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ If you are using Carthage you will need to add this to your `Cartfile` ```bash github "jpotts18/SwiftValidator" ``` +## Dependencies +CardNumberRule requires CardParser +download CardParser.swift from => https://github.com/Raizlabs/CardParser.git +Add CardParser.swift to your project before build ## Usage diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift new file mode 100644 index 0000000..46d4283 --- /dev/null +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -0,0 +1,48 @@ +// +// CardNumberRule.swift +// Validator +// +// Created by Boleigha Mark on 08/03/2018. +// Copyright © 2017 jpotts18. All rights reserved. +// +import Foundation + +/** + `CardNumberRule` is a subclass of `Rule` that defines how a credit/debit's card expiry month field is validated + */ +public class CardNumberRule: Rule { + /// Error message to be displayed if validation fails. + private var message: String + /** + Initializes `CardExpiryMonthRule` object with error message. Used to validate a card's expiry month. + + - parameter message: String of error message. + - returns: An initialized `CardExpiryMonthRule` object, or nil if an object could not be created for some reason that would not result in an exception. + */ + public init(message : String = "Card is Invalid"){ + self.message = message + } + + /** + Validates a field. + + - parameter value: String to check for validation. + - returns: Boolean value. True on successful validation, otherwise False on failed Validation. + */ + public func validate(_ value: String) -> Bool { + + guard CardState(fromNumber: value) != .invalid else { + return false + } + } + + /** + Used to display error message when validation fails. + + - returns: String of error message. + */ + public func errorMessage() -> String { + return message + } + +} \ No newline at end of file From 437bd632c982422574ceb1fd9139f9505c2c1837 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 16:27:17 +0100 Subject: [PATCH 02/18] added cardparser --- README.md | 7 +- SwiftValidator/Parsers/CardParser.swift | 228 ++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 SwiftValidator/Parsers/CardParser.swift diff --git a/README.md b/README.md index ca29b28..9880a15 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,12 @@ If you are using Carthage you will need to add this to your `Cartfile` github "jpotts18/SwiftValidator" ``` ## Dependencies -CardNumberRule requires CardParser + +CardNumberRule requires CardParser. + download CardParser.swift from => https://github.com/Raizlabs/CardParser.git -Add CardParser.swift to your project before build + +Add CardParser.swift to your project before build. ## Usage diff --git a/SwiftValidator/Parsers/CardParser.swift b/SwiftValidator/Parsers/CardParser.swift new file mode 100644 index 0000000..a5769ba --- /dev/null +++ b/SwiftValidator/Parsers/CardParser.swift @@ -0,0 +1,228 @@ + +// +// CardParser.swift +// +// Created by Jason Clark on 6/28/16. +// Copyright © 2016 Raizlabs. All rights reserved. +// +//MARK: - CardType +enum CardType { + case amex + case diners + case discover + case jcb + case masterCard + case visa + case verve + + static let allValues: [CardType] = [.visa, .masterCard, .amex, .diners, .discover, .jcb, .verve] + + private var validationRequirements: ValidationRequirement { + + let prefix: [PrefixContainable], length: [Int] + + switch self { + /* // IIN prefixes and length requriements retreived from https://en.wikipedia.org/wiki/Bank_card_number on June 28, 2016 */ + + case .amex: + prefix = ["34", "37"] + length = [15] + + case .diners: + prefix = ["300"..."305", "309", "36", "38"..."39"] + length = [14] + + case .discover: + prefix = ["6011", "65", "644"..."649", "622126"..."622925"] + length = [16] + + case .jcb: + prefix = ["3528"..."3589"] + length = [16] + + case .masterCard: + prefix = ["51"..."55", "2221"..."2720"] + length = [16] + + case .visa: + prefix = ["4"] + length = [13, 16, 19] + + case .verve: + prefix = ["5060", "5061", "5078", "5079", "6500"] + length = [16, 19] + + } + + return ValidationRequirement(prefixes: prefix, lengths: length) + } + + + + var segmentGroupings: [Int] { + switch self { + case .amex: + return [4, 6, 5] + case .diners: + return [4, 6, 4] + case .verve: + return [4, 4, 4, 6] + default: + return [4, 4, 4, 4] + } + } + + var maxLength: Int { + return validationRequirements.lengths.max() ?? 16 + } + + var cvvLength: Int { + switch self { + case .amex: return 4 + default: return 3 + } + } + + func isValid(_ accountNumber: String) -> Bool { + return validationRequirements.isValid(accountNumber) && CardType.luhnCheck(accountNumber) + } + + func isPrefixValid(_ accountNumber: String) -> Bool { + return validationRequirements.isPrefixValid(accountNumber) + } + +} + + +//MARK: Validation requirements and rules + +fileprivate extension CardType { + + struct ValidationRequirement { + let prefixes: [PrefixContainable] + let lengths: [Int] + + func isValid(_ accountNumber: String) -> Bool { + return isLengthValid(accountNumber) && isPrefixValid(accountNumber) + } + + func isPrefixValid(_ accountNumber: String) -> Bool { + guard prefixes.count > 0 else { return true } + return prefixes.contains { $0.hasCommonPrefix(with: accountNumber) } + } + + func isLengthValid(_ accountNumber: String) -> Bool { + guard lengths.count > 0 else { return true } + return lengths.contains { accountNumber.length == $0 } + } + } + + // from: https://gist.github.com/cwagdev/635ce973e8e86da0403a + static func luhnCheck(_ cardNumber: String) -> Bool { + var sum = 0 + let reversedCharacters = cardNumber.characters.reversed().map { String($0) } + for (idx, element) in reversedCharacters.enumerated() { + guard let digit = Int(element) else { return false } + switch ((idx % 2 == 1), digit) { + case (true, 9): sum += 9 + case (true, 0...8): sum += (digit * 2) % 9 + default: sum += digit + } + } + return sum % 10 == 0 + } + +} + + +//MARK: - CardState +enum CardState { + case identified(CardType) + case indeterminate([CardType]) + case invalid +} + + +extension CardState: Equatable {} + func ==(lhs: CardState, rhs: CardState) -> Bool { + switch (lhs, rhs) { + case (.invalid, .invalid): return true + case (let .indeterminate(cards1), let .indeterminate(cards2)): return cards1 == cards2 + case (let .identified(card1), let .identified(card2)): return card1 == card2 + default: return false + } +} + + +extension CardState { + + init(fromNumber number: String) { + if let card = CardType.allValues.first(where: { $0.isValid(number) }) { + self = .identified(card) + } + else { + self = .invalid + } + } + + init(fromPrefix prefix: String) { + let possibleTypes = CardType.allValues.filter { $0.isPrefixValid(prefix) } + if possibleTypes.count >= 2 { + self = .indeterminate(possibleTypes) + } + else if possibleTypes.count == 1, let card = possibleTypes.first { + self = .identified(card) + } + else { + self = .invalid + } + } + +} + +//MARK: - PrefixContainable +fileprivate protocol PrefixContainable { + + func hasCommonPrefix(with text: String) -> Bool + +} + +extension ClosedRange: PrefixContainable { + + func hasCommonPrefix(with text: String) -> Bool { + //cannot include Where clause in protocol conformance, so have to ensure Bound == String :( + guard let lower = lowerBound as? String, let upper = upperBound as? String else { return false } + + let trimmedRange: ClosedRange = { + let length = text.length + let trimmedStart = lower.prefix(length) + let trimmedEnd = upper.prefix(length) + return trimmedStart...trimmedEnd + }() + + let trimmedText = text.prefix(trimmedRange.lowerBound.count) + return trimmedRange ~= trimmedText + } + +} + + +extension String: PrefixContainable { + + func hasCommonPrefix(with text: String) -> Bool { + return hasPrefix(text) || text.hasPrefix(self) + } + +} + +fileprivate extension String { + + func prefix(_ maxLength: Int) -> String { + return String(maxLength) + } + + var length: Int { + return count + } + +} From 15d04f77c298986409a2716ea31ec2b13fa93c9f Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 16:30:14 +0100 Subject: [PATCH 03/18] Fixed validation return error on creditCardRule --- SwiftValidator/Rules/CardNumberRule.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index 46d4283..9b93286 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -34,6 +34,8 @@ public class CardNumberRule: Rule { guard CardState(fromNumber: value) != .invalid else { return false } + + return true } /** From 0a77de416b024fcf991e9718b6f184217dc4a5a4 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 16:40:23 +0100 Subject: [PATCH 04/18] fixed card returning invalid due to spaces --- SwiftValidator/Rules/CardNumberRule.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index 9b93286..dce2928 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -30,7 +30,7 @@ public class CardNumberRule: Rule { - returns: Boolean value. True on successful validation, otherwise False on failed Validation. */ public func validate(_ value: String) -> Bool { - + value = value.replacingOccurrences(of: " ", with: "") guard CardState(fromNumber: value) != .invalid else { return false } From 9c1bb96db706fd401650d62535c965d86d3f0e38 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 16:42:37 +0100 Subject: [PATCH 05/18] fixed card returning invalid due to spaces --- SwiftValidator/Rules/CardNumberRule.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index dce2928..0cd8e6f 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -30,8 +30,8 @@ public class CardNumberRule: Rule { - returns: Boolean value. True on successful validation, otherwise False on failed Validation. */ public func validate(_ value: String) -> Bool { - value = value.replacingOccurrences(of: " ", with: "") - guard CardState(fromNumber: value) != .invalid else { + cardNoFull = value.replacingOccurrences(of: " ", with: "") + guard CardState(fromNumber: cardNoFull) != .invalid else { return false } From bdc64c177bbaf1c719c73bbd90040ab14d26a0e3 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 16:43:43 +0100 Subject: [PATCH 06/18] fixed card returning invalid due to spaces --- SwiftValidator/Rules/CardNumberRule.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index 0cd8e6f..a390d5e 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -30,7 +30,7 @@ public class CardNumberRule: Rule { - returns: Boolean value. True on successful validation, otherwise False on failed Validation. */ public func validate(_ value: String) -> Bool { - cardNoFull = value.replacingOccurrences(of: " ", with: "") + let cardNoFull = value.replacingOccurrences(of: " ", with: "") guard CardState(fromNumber: cardNoFull) != .invalid else { return false } From 6389105ea0b8696d5bda8995fe58c3ac81b84c5e Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 16:51:41 +0100 Subject: [PATCH 07/18] Changed Readme to notify of CardParser Library --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9880a15..b916e4a 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,9 @@ github "jpotts18/SwiftValidator" ``` ## Dependencies -CardNumberRule requires CardParser. +CardNumberRule uses CardParser from => https://github.com/Raizlabs/CardParser.git +Cards can be validated to ensure they conform to LUHN algorigthm. -download CardParser.swift from => https://github.com/Raizlabs/CardParser.git - -Add CardParser.swift to your project before build. ## Usage From df7748faf47d75a8443a260d4469efff865cc86d Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 17:40:54 +0100 Subject: [PATCH 08/18] created Unit tests for card number validation --- SwiftValidator/Rules/CardNumberRule.swift | 1 + SwiftValidatorTests/SwiftValidatorTests.swift | 108 +++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index a390d5e..ccc439f 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -31,6 +31,7 @@ public class CardNumberRule: Rule { */ public func validate(_ value: String) -> Bool { let cardNoFull = value.replacingOccurrences(of: " ", with: "") + guard CardState(fromNumber: cardNoFull) != .invalid else { return false } diff --git a/SwiftValidatorTests/SwiftValidatorTests.swift b/SwiftValidatorTests/SwiftValidatorTests.swift index 4672e21..a9c54f2 100644 --- a/SwiftValidatorTests/SwiftValidatorTests.swift +++ b/SwiftValidatorTests/SwiftValidatorTests.swift @@ -52,7 +52,40 @@ class SwiftValidatorTests: XCTestCase { let UNREGISTER_ERRORS_TXT_FIELD = UITextField() let UNREGISTER_ERRORS_VALIDATOR = Validator() - + + /* + Card number Validation Tests + */ + + //VISA + let VALID_VISA_CARD = "4000056655665556" + let INVALID_VISA_CARD = "4960092245196342" + + //MASTERCARD + let VALID_MASTERCARD = "5399838383838381" + let INVALID_MASTERCARD = "53998383838623381" + + //VERVE(NIGERIA) + let VALID_VERVE_CARD = "5061460410120223210" + let INVALID_VERVE_CARD = "5061460622120223210" + + //AMEX + let VALID_AMEX = "344173993556638" + let INVALID_AMEX = "3441739936546638" + + //DISCOVER + let VALID_DISCOVER = "6011111111111117" + let INVALID_DISCOVER = "6011116641111117" + + //UnionPay + let VALID_UNIONPAY = "6200000000000005" + let INVALID_UNIONPAY = "62000065850000005" + + //JCB + let VALID_JCB = "3566002020360505" + let INVALID_JCB = "3566002650360505" + + let ERROR_LABEL = UILabel() override func setUp() { @@ -65,7 +98,78 @@ class SwiftValidatorTests: XCTestCase { super.tearDown() } + //MARK: CARD NUMBER VALIDATION + + //VISA + func testVisaValid(){ + XCTAssertTrue(CardNumberRule().validate(VALID_VISA_CARD), "Valid Visa Card Should Return True") + } + + func testVisaInvalid(){ + XCTAssertFalse(CardNumberRule().validate(INVALID_VISA_CARD), "Invalid Visa Card should return false") + } + + + //AMEX + func testValidAmex(){ + XCTAssertTrue(CardNumberRule().validate(VALID_AMEX), "Valid amex card should return true") + } + func testInvalidAmex(){ + XCTAssertFalse(CardNumberRule().validate(INVALID_AMEX), "Invalid Amex should return false") + } + + //MASTERCARD + func testValidMasterCard(){ + XCTAssertTrue(CardNumberRule().validate(VALID_MASTERCARD), "Valid Mastercard should return true") + } + + func testInvalidMasterCard(){ + XCTAssertFalse(CardNumberRule().validate(INVALID_MASTERCARD), "Invalid mastercard should return false") + } + + //Discover + func testValidDiscover(){ + XCTAssertTrue(CardNumberRule().validate(VALID_DISCOVER), "Valid Discover card should return true") + } + + func testInvalidDiscover(){ + XCTAssertFalse(CardNumberRule().validate(INVALID_DISCOVER), "Invalid Discover card should return false") + } + + //UNIONPAY + func testValidUnionPay(){ + XCTAssertTrue(CardNumberRule().validate(VALID_UNIONPAY), "Invalid UnionPay card should return false") + } + + //UNIONPAY + func testInvalidUnionPay(){ + XCTAssertFalse(CardNumberRule().validate(INVALID_UNIONPAY), "Valid UnionPay card should return true") + } + + //JCB + func testValidJCB(){ + XCTAssertTrue(CardNumberRule().validate(VALID_JCB), "Valid JCB card should return true") + } + + func testInvalidJCB(){ + XCTAssertFalse(CardNumberRule().validate(INVALID_JCB), "Invalid JCB card should return false") + } + + + //Verve + func testValidVerve(){ + XCTAssertTrue(CardNumberRule().validate(VALID_VERVE_CARD), "Valid Verve Card should return true") + } + + func testInvalidVerve(){ + XCTAssertFalse(CardNumberRule().validate(INVALID_VERVE_CARD), "Invalid Verve Card should return false") + } + + + + + // MARK: Expiry Month func testCardExpiryMonthValid() { @@ -483,4 +587,6 @@ class SwiftValidatorTests: XCTestCase { XCTAssert(!(self.REGISTER_TXT_FIELD.layer.borderColor! == UIColor.red.cgColor), "Color shouldn't get set at all") } } + + func test } From 73d89ae520c8faaeef6eabaed1e4ac2b41ca55de Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 17:56:25 +0100 Subject: [PATCH 09/18] Fixed credit card number validation build test --- SwiftValidator/Rules/CardNumberRule.swift | 6 +++--- SwiftValidatorTests/SwiftValidatorTests.swift | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index ccc439f..9f758ec 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -8,16 +8,16 @@ import Foundation /** - `CardNumberRule` is a subclass of `Rule` that defines how a credit/debit's card expiry month field is validated + `CardNumberRule` is a subclass of `Rule` that defines how a credit/debit's card number field is validated */ public class CardNumberRule: Rule { /// Error message to be displayed if validation fails. private var message: String /** - Initializes `CardExpiryMonthRule` object with error message. Used to validate a card's expiry month. + Initializes `CardNumberRule` object with error message. Used to validate a card's expiry month. - parameter message: String of error message. - - returns: An initialized `CardExpiryMonthRule` object, or nil if an object could not be created for some reason that would not result in an exception. + - returns: An initialized `CardNumberRule` object, or nil if an object could not be created for some reason that would not result in an exception. */ public init(message : String = "Card is Invalid"){ self.message = message diff --git a/SwiftValidatorTests/SwiftValidatorTests.swift b/SwiftValidatorTests/SwiftValidatorTests.swift index a9c54f2..ce219e9 100644 --- a/SwiftValidatorTests/SwiftValidatorTests.swift +++ b/SwiftValidatorTests/SwiftValidatorTests.swift @@ -587,6 +587,4 @@ class SwiftValidatorTests: XCTestCase { XCTAssert(!(self.REGISTER_TXT_FIELD.layer.borderColor! == UIColor.red.cgColor), "Color shouldn't get set at all") } } - - func test } From 27c27423d71e95e16643e0988fc606fff864e6f0 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 18:14:05 +0100 Subject: [PATCH 10/18] testing card validation --- SwiftValidator/Rules/CardNumberRule.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index 9f758ec..3387a9d 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -12,7 +12,7 @@ import Foundation */ public class CardNumberRule: Rule { /// Error message to be displayed if validation fails. - private var message: String + private var message : String /** Initializes `CardNumberRule` object with error message. Used to validate a card's expiry month. From 168a5b0c58628cb871da22d443b70c142fbe0873 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 8 Mar 2019 18:39:36 +0100 Subject: [PATCH 11/18] resolved group linking causing build errors --- SwiftValidator/Rules/CardNumberRule.swift | 16 ++++++++-------- Validator.xcodeproj/project.pbxproj | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index 3387a9d..27dba3d 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -30,13 +30,13 @@ public class CardNumberRule: Rule { - returns: Boolean value. True on successful validation, otherwise False on failed Validation. */ public func validate(_ value: String) -> Bool { - let cardNoFull = value.replacingOccurrences(of: " ", with: "") - - guard CardState(fromNumber: cardNoFull) != .invalid else { - return false - } - - return true + let cardNoFull = value.replacingOccurrences(of: " ", with: "") + + guard CardState(fromNumber: cardNoFull) != .invalid else { + return false + } + + return true } /** @@ -48,4 +48,4 @@ public class CardNumberRule: Rule { return message } -} \ No newline at end of file +} diff --git a/Validator.xcodeproj/project.pbxproj b/Validator.xcodeproj/project.pbxproj index fb03e8a..dfac948 100644 --- a/Validator.xcodeproj/project.pbxproj +++ b/Validator.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 256576A72232DFD900C8369F /* CardNumberRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 256576A62232DFD900C8369F /* CardNumberRule.swift */; }; + 256576AA2232E01500C8369F /* CardParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 256576A92232E01500C8369F /* CardParser.swift */; }; 62C1821D1C6312F5003788E7 /* ExactLengthRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C1821C1C6312F5003788E7 /* ExactLengthRule.swift */; }; 62D1AE1D1A1E6D4400E4DFF8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62D1AE1C1A1E6D4400E4DFF8 /* AppDelegate.swift */; }; 62D1AE221A1E6D4400E4DFF8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 62D1AE201A1E6D4400E4DFF8 /* Main.storyboard */; }; @@ -92,6 +94,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 256576A62232DFD900C8369F /* CardNumberRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardNumberRule.swift; sourceTree = ""; }; + 256576A92232E01500C8369F /* CardParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardParser.swift; sourceTree = ""; }; 62C1821C1C6312F5003788E7 /* ExactLengthRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExactLengthRule.swift; sourceTree = ""; }; 62D1AE171A1E6D4400E4DFF8 /* Validator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Validator.app; sourceTree = BUILT_PRODUCTS_DIR; }; 62D1AE1B1A1E6D4400E4DFF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -170,6 +174,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 256576A82232E01500C8369F /* Parsers */ = { + isa = PBXGroup; + children = ( + 256576A92232E01500C8369F /* CardParser.swift */, + ); + name = Parsers; + path = SwiftValidator/Parsers; + sourceTree = SOURCE_ROOT; + }; 62D1AE0E1A1E6D4400E4DFF8 = { isa = PBXGroup; children = ( @@ -290,6 +303,8 @@ FB465CEE1B9889EA00398388 /* ValidationRule.swift */, FB465CEF1B9889EA00398388 /* ZipCodeRule.swift */, 62C1821C1C6312F5003788E7 /* ExactLengthRule.swift */, + 256576A62232DFD900C8369F /* CardNumberRule.swift */, + 256576A82232E01500C8369F /* Parsers */, ); path = Rules; sourceTree = ""; @@ -512,6 +527,7 @@ FB465CFA1B9889EA00398388 /* PhoneNumberRule.swift in Sources */, FB465CF51B9889EA00398388 /* FloatRule.swift in Sources */, C87F606C1E2B68C900EB8429 /* CardExpiryYearRule.swift in Sources */, + 256576AA2232E01500C8369F /* CardParser.swift in Sources */, 7CC1E4DB1C63BFA600AF013C /* HexColorRule.swift in Sources */, FB465D011B9889EA00398388 /* Validator.swift in Sources */, FB465CFE1B9889EA00398388 /* ValidationRule.swift in Sources */, @@ -519,6 +535,7 @@ FB465CF31B9889EA00398388 /* ConfirmRule.swift in Sources */, FB51E5B01CD208B8004DE696 /* Validatable.swift in Sources */, 7CC1E4D51C637C8500AF013C /* IPV4Rule.swift in Sources */, + 256576A72232DFD900C8369F /* CardNumberRule.swift in Sources */, 7CC1E4D71C637F6E00AF013C /* ISBNRule.swift in Sources */, FB465D001B9889EA00398388 /* ValidationError.swift in Sources */, FB465CFC1B9889EA00398388 /* RequiredRule.swift in Sources */, From bd3eb114f4f48113364559f5031d8c84a4a5880a Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Sat, 9 Mar 2019 09:23:54 +0100 Subject: [PATCH 12/18] fixed Unit tests and folder group for xcode --- SwiftValidator/Parsers/CardParser.swift | 49 +++++++++++++------ SwiftValidator/Rules/CardNumberRule.swift | 11 ++++- SwiftValidatorTests/SwiftValidatorTests.swift | 24 +++------ 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/SwiftValidator/Parsers/CardParser.swift b/SwiftValidator/Parsers/CardParser.swift index a5769ba..b98bfe8 100644 --- a/SwiftValidator/Parsers/CardParser.swift +++ b/SwiftValidator/Parsers/CardParser.swift @@ -11,11 +11,11 @@ enum CardType { case diners case discover case jcb - case masterCard + case mastercard case visa case verve - static let allValues: [CardType] = [.visa, .masterCard, .amex, .diners, .discover, .jcb, .verve] + static let allValues: [CardType] = [.visa, .mastercard, .amex, .diners, .discover, .jcb, .verve] private var validationRequirements: ValidationRequirement { @@ -27,30 +27,37 @@ enum CardType { case .amex: prefix = ["34", "37"] length = [15] + break case .diners: prefix = ["300"..."305", "309", "36", "38"..."39"] length = [14] + break case .discover: prefix = ["6011", "65", "644"..."649", "622126"..."622925"] length = [16] + break case .jcb: prefix = ["3528"..."3589"] length = [16] + break - case .masterCard: + case .mastercard: prefix = ["51"..."55", "2221"..."2720"] length = [16] + break case .visa: prefix = ["4"] length = [13, 16, 19] + break case .verve: prefix = ["5060", "5061", "5078", "5079", "6500"] length = [16, 19] + break } @@ -78,8 +85,10 @@ enum CardType { var cvvLength: Int { switch self { - case .amex: return 4 - default: return 3 + case .amex: + return 4 + default: + return 3 } } @@ -107,6 +116,7 @@ fileprivate extension CardType { } func isPrefixValid(_ accountNumber: String) -> Bool { + guard prefixes.count > 0 else { return true } return prefixes.contains { $0.hasCommonPrefix(with: accountNumber) } } @@ -119,17 +129,27 @@ fileprivate extension CardType { // from: https://gist.github.com/cwagdev/635ce973e8e86da0403a static func luhnCheck(_ cardNumber: String) -> Bool { - var sum = 0 - let reversedCharacters = cardNumber.characters.reversed().map { String($0) } - for (idx, element) in reversedCharacters.enumerated() { - guard let digit = Int(element) else { return false } - switch ((idx % 2 == 1), digit) { - case (true, 9): sum += 9 - case (true, 0...8): sum += (digit * 2) % 9 - default: sum += digit + + guard let _ = Int64(cardNumber) else { + //if string is not convertible to int, return false + return false + } + let numberOfChars = cardNumber.count + let numberToCheck = numberOfChars % 2 == 0 ? cardNumber : "0" + cardNumber + + let digits = numberToCheck.map { Int(String($0)) } + + let sum = digits.enumerated().reduce(0) { (sum, val: (offset: Int, element: Int?)) in + if (val.offset + 1) % 2 == 1 { + let element = val.element! + return sum + (element == 9 ? 9 : (element * 2) % 9) } + // else + return sum + val.element! } - return sum % 10 == 0 + let validates = sum % 10 == 0 + print("card valid") + return validates } } @@ -158,6 +178,7 @@ extension CardState { init(fromNumber number: String) { if let card = CardType.allValues.first(where: { $0.isValid(number) }) { + print("card found \(card)") self = .identified(card) } else { diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index 27dba3d..0bd3c7b 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -31,12 +31,21 @@ public class CardNumberRule: Rule { */ public func validate(_ value: String) -> Bool { let cardNoFull = value.replacingOccurrences(of: " ", with: "") - guard CardState(fromNumber: cardNoFull) != .invalid else { return false } +// let cardState = CardState(fromNumber: cardNoFull) +// switch cardState { +// case .identified(let cardType): +// print(cardType) +// case .indeterminate: +// print("undefined") +// case .invalid: +// print("invalid") +// } return true + } /** diff --git a/SwiftValidatorTests/SwiftValidatorTests.swift b/SwiftValidatorTests/SwiftValidatorTests.swift index ce219e9..f7b7140 100644 --- a/SwiftValidatorTests/SwiftValidatorTests.swift +++ b/SwiftValidatorTests/SwiftValidatorTests.swift @@ -62,28 +62,26 @@ class SwiftValidatorTests: XCTestCase { let INVALID_VISA_CARD = "4960092245196342" //MASTERCARD - let VALID_MASTERCARD = "5399838383838381" + let VALID_MASTERCARD = "5105105105105100" let INVALID_MASTERCARD = "53998383838623381" //VERVE(NIGERIA) let VALID_VERVE_CARD = "5061460410120223210" - let INVALID_VERVE_CARD = "5061460622120223210" + let INVALID_VERVE_CARD = "5061435662036050587" //AMEX let VALID_AMEX = "344173993556638" let INVALID_AMEX = "3441739936546638" //DISCOVER - let VALID_DISCOVER = "6011111111111117" + let VALID_DISCOVER = "6011000990139424" let INVALID_DISCOVER = "6011116641111117" - //UnionPay - let VALID_UNIONPAY = "6200000000000005" - let INVALID_UNIONPAY = "62000065850000005" + //JCB - let VALID_JCB = "3566002020360505" - let INVALID_JCB = "3566002650360505" + let VALID_JCB = "3566111111111113" + let INVALID_JCB = "3566754297360505" let ERROR_LABEL = UILabel() @@ -137,15 +135,7 @@ class SwiftValidatorTests: XCTestCase { XCTAssertFalse(CardNumberRule().validate(INVALID_DISCOVER), "Invalid Discover card should return false") } - //UNIONPAY - func testValidUnionPay(){ - XCTAssertTrue(CardNumberRule().validate(VALID_UNIONPAY), "Invalid UnionPay card should return false") - } - - //UNIONPAY - func testInvalidUnionPay(){ - XCTAssertFalse(CardNumberRule().validate(INVALID_UNIONPAY), "Valid UnionPay card should return true") - } + //JCB func testValidJCB(){ From f99a46c669b70288d98196ba668d3a34e84861c9 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 15 Mar 2019 09:46:45 +0100 Subject: [PATCH 13/18] Card Expiry Date in full (eg: 10/12) rule added --- SwiftValidator/Rules/CardExpiryRule.swift | 61 +++++++++++++++++++ SwiftValidatorTests/SwiftValidatorTests.swift | 19 +++++- Validator.xcodeproj/project.pbxproj | 4 ++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 SwiftValidator/Rules/CardExpiryRule.swift diff --git a/SwiftValidator/Rules/CardExpiryRule.swift b/SwiftValidator/Rules/CardExpiryRule.swift new file mode 100644 index 0000000..012edb5 --- /dev/null +++ b/SwiftValidator/Rules/CardExpiryRule.swift @@ -0,0 +1,61 @@ +// +// CardExpiryRule.swift +// SwiftValidator +// +// Created by Sprinthub on 13/03/2019. +// Copyright © 2019 jpotts18. All rights reserved. +// + +import Foundation + + + +/** + `CardExpiryRule` is a subclass of `Rule` that defines how a credit/debit's card number field is validated + */ +public class CardExpiryRule: Rule { + /// Error message to be displayed if validation fails. + private var message : String + /** + Initializes `CardNumberRule` object with error message. Used to validate a card's expiry month. + + - parameter message: String of error message. + - returns: An initialized `CardNumberRule` object, or nil if an object could not be created for some reason that would not result in an exception. + */ + public init(message : String = "Card expiry date is invalid"){ + self.message = message + } + + /** + Validates a field. + + - parameter value: String to check for validation. + - returns: Boolean value. True on successful validation, otherwise False on failed Validation. + */ + public func validate(_ value: String) -> Bool { + let date = value.replacingOccurrences(of: "/", with: "") + let Index = date.index(date.startIndex, offsetBy: 2) + //let yearIndex = date.index(date.endIndex, offsetBy: -2) + let Month = Int(date[..= thisYearTwoDigits + + } + + /** + Used to display error message when validation fails. + + - returns: String of error message. + */ + public func errorMessage() -> String { + return message + } + +} diff --git a/SwiftValidatorTests/SwiftValidatorTests.swift b/SwiftValidatorTests/SwiftValidatorTests.swift index f7b7140..66d5d2e 100644 --- a/SwiftValidatorTests/SwiftValidatorTests.swift +++ b/SwiftValidatorTests/SwiftValidatorTests.swift @@ -34,7 +34,7 @@ class SwiftValidatorTests: XCTestCase { let VALID_CARD_EXPIRY_MONTH = "10" let INVALID_CARD_EXPIRY_MONTH = "13" - let VALID_CARD_EXPIRY_YEAR = "2018" + let VALID_CARD_EXPIRY_YEAR = "2020" let INVALID_CARD_EXPIRY_YEAR = "2016" let LEN_3 = "hey" @@ -96,6 +96,10 @@ class SwiftValidatorTests: XCTestCase { super.tearDown() } + //CARD EXPIRY VALIDATION VALUES + let VALID_DATE = "10/29" + let INVALID_DATE = "10/12" + //MARK: CARD NUMBER VALIDATION //VISA @@ -188,6 +192,19 @@ class SwiftValidatorTests: XCTestCase { XCTAssertNotNil(CardExpiryYearRule().errorMessage()) } + // MARK: CARD EXPIRY DATE + func testValidCardExpiryDateFull(){ + XCTAssertTrue(CardExpiryRule().validate(VALID_DATE), "Valid card expiry date should retun true") + } + + func testInvalidCardExpiryDateFull(){ + XCTAssertFalse(CardExpiryRule().validate(INVALID_DATE), "Invalid card expiry date should return false") + } + + func testInvalidCardExpiryDateFullMessage(){ + XCTAssertNotNil(CardExpiryYearRule().errorMessage()) + } + // MARK: Required diff --git a/Validator.xcodeproj/project.pbxproj b/Validator.xcodeproj/project.pbxproj index dfac948..8cba319 100644 --- a/Validator.xcodeproj/project.pbxproj +++ b/Validator.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 256576A72232DFD900C8369F /* CardNumberRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 256576A62232DFD900C8369F /* CardNumberRule.swift */; }; 256576AA2232E01500C8369F /* CardParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 256576A92232E01500C8369F /* CardParser.swift */; }; + 25FB0A2A22395AFA00373197 /* CardExpiryRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25FB0A2922395AFA00373197 /* CardExpiryRule.swift */; }; 62C1821D1C6312F5003788E7 /* ExactLengthRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C1821C1C6312F5003788E7 /* ExactLengthRule.swift */; }; 62D1AE1D1A1E6D4400E4DFF8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62D1AE1C1A1E6D4400E4DFF8 /* AppDelegate.swift */; }; 62D1AE221A1E6D4400E4DFF8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 62D1AE201A1E6D4400E4DFF8 /* Main.storyboard */; }; @@ -96,6 +97,7 @@ /* Begin PBXFileReference section */ 256576A62232DFD900C8369F /* CardNumberRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardNumberRule.swift; sourceTree = ""; }; 256576A92232E01500C8369F /* CardParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardParser.swift; sourceTree = ""; }; + 25FB0A2922395AFA00373197 /* CardExpiryRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardExpiryRule.swift; sourceTree = ""; }; 62C1821C1C6312F5003788E7 /* ExactLengthRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExactLengthRule.swift; sourceTree = ""; }; 62D1AE171A1E6D4400E4DFF8 /* Validator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Validator.app; sourceTree = BUILT_PRODUCTS_DIR; }; 62D1AE1B1A1E6D4400E4DFF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -300,6 +302,7 @@ FB465CEB1B9889EA00398388 /* RegexRule.swift */, FB465CEC1B9889EA00398388 /* RequiredRule.swift */, FB465CED1B9889EA00398388 /* Rule.swift */, + 25FB0A2922395AFA00373197 /* CardExpiryRule.swift */, FB465CEE1B9889EA00398388 /* ValidationRule.swift */, FB465CEF1B9889EA00398388 /* ZipCodeRule.swift */, 62C1821C1C6312F5003788E7 /* ExactLengthRule.swift */, @@ -538,6 +541,7 @@ 256576A72232DFD900C8369F /* CardNumberRule.swift in Sources */, 7CC1E4D71C637F6E00AF013C /* ISBNRule.swift in Sources */, FB465D001B9889EA00398388 /* ValidationError.swift in Sources */, + 25FB0A2A22395AFA00373197 /* CardExpiryRule.swift in Sources */, FB465CFC1B9889EA00398388 /* RequiredRule.swift in Sources */, FB465CFB1B9889EA00398388 /* RegexRule.swift in Sources */, 7CC1E4CF1C636B4500AF013C /* AlphaRule.swift in Sources */, From 91ef7c387185ea068d6c9f67a961b547e00342c2 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Fri, 15 Mar 2019 09:53:11 +0100 Subject: [PATCH 14/18] minor fixes in comments --- SwiftValidator/Rules/CardExpiryRule.swift | 8 ++++---- SwiftValidator/Rules/CardNumberRule.swift | 9 --------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/SwiftValidator/Rules/CardExpiryRule.swift b/SwiftValidator/Rules/CardExpiryRule.swift index 012edb5..6977392 100644 --- a/SwiftValidator/Rules/CardExpiryRule.swift +++ b/SwiftValidator/Rules/CardExpiryRule.swift @@ -2,7 +2,7 @@ // CardExpiryRule.swift // SwiftValidator // -// Created by Sprinthub on 13/03/2019. +// Created by Mark Boleigha on 13/03/2019. // Copyright © 2019 jpotts18. All rights reserved. // @@ -17,10 +17,10 @@ public class CardExpiryRule: Rule { /// Error message to be displayed if validation fails. private var message : String /** - Initializes `CardNumberRule` object with error message. Used to validate a card's expiry month. + Initializes `CardExpiryRule` object with error message. Used to validate a card's expiry year. - parameter message: String of error message. - - returns: An initialized `CardNumberRule` object, or nil if an object could not be created for some reason that would not result in an exception. + - returns: An initialized `CardExpiryRule` object, or nil if an object could not be created for some reason that would not result in an exception. */ public init(message : String = "Card expiry date is invalid"){ self.message = message @@ -29,7 +29,7 @@ public class CardExpiryRule: Rule { /** Validates a field. - - parameter value: String to check for validation. + - parameter value: String to check for validation. must be a card expiry date in MM/YY format - returns: Boolean value. True on successful validation, otherwise False on failed Validation. */ public func validate(_ value: String) -> Bool { diff --git a/SwiftValidator/Rules/CardNumberRule.swift b/SwiftValidator/Rules/CardNumberRule.swift index 0bd3c7b..4ed8b49 100644 --- a/SwiftValidator/Rules/CardNumberRule.swift +++ b/SwiftValidator/Rules/CardNumberRule.swift @@ -34,15 +34,6 @@ public class CardNumberRule: Rule { guard CardState(fromNumber: cardNoFull) != .invalid else { return false } -// let cardState = CardState(fromNumber: cardNoFull) -// switch cardState { -// case .identified(let cardType): -// print(cardType) -// case .indeterminate: -// print("undefined") -// case .invalid: -// print("invalid") -// } return true From c0e5af70f90ec9c9250234c67bd8655ffb2b61ff Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Mon, 18 Mar 2019 13:52:31 +0100 Subject: [PATCH 15/18] removed comment line --- SwiftValidator/Rules/CardExpiryRule.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/SwiftValidator/Rules/CardExpiryRule.swift b/SwiftValidator/Rules/CardExpiryRule.swift index 6977392..21613bf 100644 --- a/SwiftValidator/Rules/CardExpiryRule.swift +++ b/SwiftValidator/Rules/CardExpiryRule.swift @@ -35,7 +35,6 @@ public class CardExpiryRule: Rule { public func validate(_ value: String) -> Bool { let date = value.replacingOccurrences(of: "/", with: "") let Index = date.index(date.startIndex, offsetBy: 2) - //let yearIndex = date.index(date.endIndex, offsetBy: -2) let Month = Int(date[.. Date: Mon, 18 Mar 2019 14:07:04 +0100 Subject: [PATCH 16/18] fixed month not validating past November --- SwiftValidator/Rules/CardExpiryRule.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftValidator/Rules/CardExpiryRule.swift b/SwiftValidator/Rules/CardExpiryRule.swift index 21613bf..7e35280 100644 --- a/SwiftValidator/Rules/CardExpiryRule.swift +++ b/SwiftValidator/Rules/CardExpiryRule.swift @@ -44,7 +44,7 @@ public class CardExpiryRule: Rule { let thisYearTwoDigits = Int(thisYear[thisYearLast2...])! - return Month! < 12 && Year! >= thisYearTwoDigits + return Month! <= 12 && Year! >= thisYearTwoDigits } From 4d61e5ce0b97122397a3be95ba01bf223a786fa6 Mon Sep 17 00:00:00 2001 From: Mark Boleigha Date: Mon, 18 Mar 2019 15:04:36 +0100 Subject: [PATCH 17/18] added checks for blank card expiry date causing crash --- SwiftValidator/Rules/CardExpiryRule.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SwiftValidator/Rules/CardExpiryRule.swift b/SwiftValidator/Rules/CardExpiryRule.swift index 7e35280..2bf969d 100644 --- a/SwiftValidator/Rules/CardExpiryRule.swift +++ b/SwiftValidator/Rules/CardExpiryRule.swift @@ -33,6 +33,9 @@ public class CardExpiryRule: Rule { - returns: Boolean value. True on successful validation, otherwise False on failed Validation. */ public func validate(_ value: String) -> Bool { + guard value.count > 4 else{ + return false + } let date = value.replacingOccurrences(of: "/", with: "") let Index = date.index(date.startIndex, offsetBy: 2) let Month = Int(date[.. Date: Mon, 18 Mar 2019 15:13:54 +0100 Subject: [PATCH 18/18] fixed endIndex error in expiry date validation --- SwiftValidator/Rules/CardExpiryRule.swift | 8 +++++--- SwiftValidatorTests/SwiftValidatorTests.swift | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/SwiftValidator/Rules/CardExpiryRule.swift b/SwiftValidator/Rules/CardExpiryRule.swift index 2bf969d..7dc967d 100644 --- a/SwiftValidator/Rules/CardExpiryRule.swift +++ b/SwiftValidator/Rules/CardExpiryRule.swift @@ -37,9 +37,11 @@ public class CardExpiryRule: Rule { return false } let date = value.replacingOccurrences(of: "/", with: "") - let Index = date.index(date.startIndex, offsetBy: 2) - let Month = Int(date[..