From 3f35b78b4060e3c2303e81e9bb2153fbee4d3988 Mon Sep 17 00:00:00 2001 From: Calum Lind Date: Wed, 31 May 2023 09:08:39 +0100 Subject: [PATCH] [PM-2256] Fix importer parsing credit card expiry year (#5444) * Fix importer parsing credit card expiry year When importing a credit card from Enpass it was found that with a 4 digit expiry year was prefixed with '20', stored at 11/202025 instead of 11/2025. Fixed typo that checked length of month instead of year which incorrectly added prefix. * Refactor setCardExpiration to use RegExp --- libs/importer/spec/base-importer.spec.ts | 107 ++++++++++++++++++ .../spec/enpass-json-importer.spec.ts | 2 +- .../spec/nordpass-csv-importer.spec.ts | 2 +- libs/importer/src/importers/base-importer.ts | 41 ++++--- 4 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 libs/importer/spec/base-importer.spec.ts diff --git a/libs/importer/spec/base-importer.spec.ts b/libs/importer/spec/base-importer.spec.ts new file mode 100644 index 0000000000..db98872142 --- /dev/null +++ b/libs/importer/spec/base-importer.spec.ts @@ -0,0 +1,107 @@ +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { BaseImporter } from "../src/importers/base-importer"; + +class FakeBaseImporter extends BaseImporter { + initLoginCipher(): CipherView { + return super.initLoginCipher(); + } + + setCardExpiration(cipher: CipherView, expiration: string): boolean { + return super.setCardExpiration(cipher, expiration); + } +} + +describe("BaseImporter class", () => { + const importer = new FakeBaseImporter(); + let cipher: CipherView; + + describe("setCardExpiration method", () => { + beforeEach(() => { + cipher = importer.initLoginCipher(); + cipher.card = new CardView(); + cipher.type = CipherType.Card; + }); + + it.each([ + ["01/2025", "1", "2025"], + ["5/21", "5", "2021"], + ["10/2100", "10", "2100"], + ])( + "sets ciper card expYear & expMonth and returns true", + (expiration, expectedMonth, expectedYear) => { + const result = importer.setCardExpiration(cipher, expiration); + expect(cipher.card.expMonth).toBe(expectedMonth); + expect(cipher.card.expYear).toBe(expectedYear); + expect(result).toBe(true); + } + ); + + it.each([ + ["01/2032", "1"], + ["09/2032", "9"], + ["10/2032", "10"], + ])("removes leading zero from month", (expiration, expectedMonth) => { + const result = importer.setCardExpiration(cipher, expiration); + expect(cipher.card.expMonth).toBe(expectedMonth); + expect(cipher.card.expYear).toBe("2032"); + expect(result).toBe(true); + }); + + it.each([ + ["12/00", "2000"], + ["12/99", "2099"], + ["12/32", "2032"], + ["12/2042", "2042"], + ])("prefixes '20' to year if only two digits long", (expiration, expectedYear) => { + const result = importer.setCardExpiration(cipher, expiration); + expect(cipher.card.expYear).toHaveLength(4); + expect(cipher.card.expYear).toBe(expectedYear); + expect(result).toBe(true); + }); + + it.each([["01 / 2025"], ["01 / 2025"], [" 01/2025 "], [" 01/2025 "]])( + "removes any whitespace in expiration string", + (expiration) => { + const result = importer.setCardExpiration(cipher, expiration); + expect(cipher.card.expMonth).toBe("1"); + expect(cipher.card.expYear).toBe("2025"); + expect(result).toBe(true); + } + ); + + it.each([[""], [" "], [null]])( + "returns false if expiration is null or empty ", + (expiration) => { + const result = importer.setCardExpiration(cipher, expiration); + expect(result).toBe(false); + } + ); + + it.each([["0123"], ["01/03/23"]])( + "returns false if invalid card expiration string", + (expiration) => { + const result = importer.setCardExpiration(cipher, expiration); + expect(result).toBe(false); + } + ); + + it.each([["5/"], ["03/231"], ["12/1"], ["2/20221"]])( + "returns false if year is not 2 or 4 digits long", + (expiration) => { + const result = importer.setCardExpiration(cipher, expiration); + expect(result).toBe(false); + } + ); + + it.each([["/2023"], ["003/2023"], ["111/32"]])( + "returns false if month is not 1 or 2 digits long", + (expiration) => { + const result = importer.setCardExpiration(cipher, expiration); + expect(result).toBe(false); + } + ); + }); +}); diff --git a/libs/importer/spec/enpass-json-importer.spec.ts b/libs/importer/spec/enpass-json-importer.spec.ts index f23ace33e2..0c45746de0 100644 --- a/libs/importer/spec/enpass-json-importer.spec.ts +++ b/libs/importer/spec/enpass-json-importer.spec.ts @@ -100,7 +100,7 @@ describe("Enpass JSON Importer", () => { expect(cipher.card.brand).toEqual("Amex"); expect(cipher.card.code).toEqual("1234"); expect(cipher.card.expMonth).toEqual("3"); - expect(cipher.card.expYear).toEqual("23"); + expect(cipher.card.expYear).toEqual("2023"); // remaining fields as custom fields expect(cipher.fields.length).toEqual(9); diff --git a/libs/importer/spec/nordpass-csv-importer.spec.ts b/libs/importer/spec/nordpass-csv-importer.spec.ts index 8e681262a5..b4e2ca457c 100644 --- a/libs/importer/spec/nordpass-csv-importer.spec.ts +++ b/libs/importer/spec/nordpass-csv-importer.spec.ts @@ -77,7 +77,7 @@ function expectCreditCard(cipher: CipherView) { expect(cipher.card.number).toBe("4024007103939509"); expect(cipher.card.code).toBe("123"); expect(cipher.card.expMonth).toBe("1"); - expect(cipher.card.expYear).toBe("22"); + expect(cipher.card.expYear).toBe("2022"); } function expectIdentity(cipher: CipherView) { diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index 7423f4adba..e416142abf 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -305,29 +305,26 @@ export abstract class BaseImporter { } protected setCardExpiration(cipher: CipherView, expiration: string): boolean { - if (!this.isNullOrWhitespace(expiration)) { - expiration = expiration.replace(/\s/g, ""); - const parts = expiration.split("/"); - if (parts.length === 2) { - let month: string = null; - let year: string = null; - if (parts[0].length === 1 || parts[0].length === 2) { - month = parts[0]; - if (month.length === 2 && month[0] === "0") { - month = month.substr(1, 1); - } - } - if (parts[1].length === 2 || parts[1].length === 4) { - year = month.length === 2 ? "20" + parts[1] : parts[1]; - } - if (month != null && year != null) { - cipher.card.expMonth = month; - cipher.card.expYear = year; - return true; - } - } + if (this.isNullOrWhitespace(expiration)) { + return false; } - return false; + + expiration = expiration.replace(/\s/g, ""); + + const monthRegex = "0?(?[1-9]|1[0-2])"; + const yearRegex = "(?(?:[1-2][0-9])?[0-9]{2})"; + const expiryRegex = new RegExp(`^${monthRegex}/${yearRegex}$`); + + const expiryMatch = expiration.match(expiryRegex); + + if (!expiryMatch) { + return false; + } + + cipher.card.expMonth = expiryMatch.groups.month; + const year: string = expiryMatch.groups.year; + cipher.card.expYear = year.length === 2 ? "20" + year : year; + return true; } protected moveFoldersToCollections(result: ImportResult) {