1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-11350] Use shared expiration year normalization util function (#10735)

* use shared expiration year normalization util function

* use shared exp year normalization in web and desktop client

* handle cases where input has leading zeroes

* add utils tests

* handle cases where input is all zeroes
This commit is contained in:
Jonathan Prusik
2024-09-03 15:12:36 -04:00
committed by GitHub
parent b27dc44298
commit 5f2eecd7be
8 changed files with 142 additions and 12 deletions

View File

@@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
import { CardLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
import { normalizeExpiryYearFormat } from "../../utils";
import { ItemView } from "./item.view";
@@ -65,17 +66,16 @@ export class CardView extends ItemView {
}
get expiration(): string {
if (!this.expMonth && !this.expYear) {
const normalizedYear = normalizeExpiryYearFormat(this.expYear);
if (!this.expMonth && !normalizedYear) {
return null;
}
let exp = this.expMonth != null ? ("0" + this.expMonth).slice(-2) : "__";
exp += " / " + (this.expYear != null ? this.formatYear(this.expYear) : "____");
return exp;
}
exp += " / " + (normalizedYear || "____");
private formatYear(year: string): string {
return year.length === 2 ? "20" + year : year;
return exp;
}
static fromJSON(obj: Partial<Jsonify<CardView>>): CardView {

View File

@@ -0,0 +1,74 @@
import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
function getExpiryYearValueFormats(currentCentury: string) {
return [
[-12, `${currentCentury}12`],
[0, `${currentCentury}00`],
[2043, "2043"], // valid year with a length of four should be taken directly
[24, `${currentCentury}24`],
[3054, "3054"], // valid year with a length of four should be taken directly
[31423524543, `${currentCentury}43`],
[4, `${currentCentury}04`],
[null, null],
[undefined, null],
["-12", `${currentCentury}12`],
["", null],
["0", `${currentCentury}00`],
["00", `${currentCentury}00`],
["000", `${currentCentury}00`],
["0000", `${currentCentury}00`],
["00000", `${currentCentury}00`],
["0234234", `${currentCentury}34`],
["04", `${currentCentury}04`],
["2043", "2043"], // valid year with a length of four should be taken directly
["24", `${currentCentury}24`],
["3054", "3054"], // valid year with a length of four should be taken directly
["31423524543", `${currentCentury}43`],
["4", `${currentCentury}04`],
["aaaa", null],
["adgshsfhjsdrtyhsrth", null],
["agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", `${currentCentury}45`],
];
}
describe("normalizeExpiryYearFormat", () => {
const currentCentury = `${new Date().getFullYear()}`.slice(0, 2);
const expiryYearValueFormats = getExpiryYearValueFormats(currentCentury);
expiryYearValueFormats.forEach(([inputValue, expectedValue]) => {
it(`should return '${expectedValue}' when '${inputValue}' is passed`, () => {
const formattedValue = normalizeExpiryYearFormat(inputValue);
expect(formattedValue).toEqual(expectedValue);
});
});
describe("in the year 3107", () => {
const theDistantFuture = new Date(Date.UTC(3107, 1, 1));
jest.spyOn(Date, "now").mockReturnValue(theDistantFuture.valueOf());
beforeAll(() => {
jest.useFakeTimers({ advanceTimers: true });
jest.setSystemTime(theDistantFuture);
});
afterAll(() => {
jest.useRealTimers();
});
const currentCentury = `${new Date(Date.now()).getFullYear()}`.slice(0, 2);
expect(currentCentury).toBe("31");
const expiryYearValueFormats = getExpiryYearValueFormats(currentCentury);
expiryYearValueFormats.forEach(([inputValue, expectedValue]) => {
it(`should return '${expectedValue}' when '${inputValue}' is passed`, () => {
const formattedValue = normalizeExpiryYearFormat(inputValue);
expect(formattedValue).toEqual(expectedValue);
});
});
jest.clearAllTimers();
});
});

View File

@@ -0,0 +1,42 @@
type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`;
/**
* Takes a string or number value and returns a string value formatted as a valid 4-digit year
*
* @export
* @param {(string | number)} yearInput
* @return {*} {(Year | null)}
*/
export function normalizeExpiryYearFormat(yearInput: string | number): Year | null {
// The input[type="number"] is returning a number, convert it to a string
// An empty field returns null, avoid casting `"null"` to a string
const yearInputIsEmpty = yearInput == null || yearInput === "";
let expirationYear = yearInputIsEmpty ? null : `${yearInput}`;
// Exit early if year is already formatted correctly or empty
if (yearInputIsEmpty || /^[1-9]{1}\d{3}$/.test(expirationYear)) {
return expirationYear as Year;
}
expirationYear = expirationYear
// For safety, because even input[type="number"] will allow decimals
.replace(/[^\d]/g, "")
// remove any leading zero padding (leave the last leading zero if it ends the string)
.replace(/^[0]+(?=.)/, "");
if (expirationYear === "") {
expirationYear = null;
}
// given the context of payment card expiry, a year character length of 3, or over 4
// is more likely to be a mistake than an intentional value for the far past or far future.
if (expirationYear && expirationYear.length !== 4) {
const paddedYear = ("00" + expirationYear).slice(-2);
const currentCentury = `${new Date().getFullYear()}`.slice(0, 2);
expirationYear = currentCentury + paddedYear;
}
return expirationYear as Year | null;
}