chromium/chrome/test/data/webui/settings/autofill_fake_data.ts

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// clang-format off
import type {AutofillManagerProxy, PaymentsManagerProxy, PersonalDataChangedListener} from 'chrome://settings/lazy_load.js';
import {assertEquals, assertFalse} from 'chrome://webui-test/chai_assert.js';
import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';

// clang-format on

const FieldType = chrome.autofillPrivate.FieldType;

export const STUB_USER_ACCOUNT_INFO: chrome.autofillPrivate.AccountInfo = {
  email: '[email protected]',
  isSyncEnabledForAutofillProfiles: false,
  isEligibleForAddressAccountStorage: false,
  isAutofillSyncToggleAvailable: false,
  isAutofillSyncToggleEnabled: false,
};

/**
 * Creates a new fake address entry for testing.
 */
export function createEmptyAddressEntry(): chrome.autofillPrivate.AddressEntry {
  return {
    fields: [],
  };
}

/**
 * Creates a fake address entry for testing.
 */
export function createAddressEntry(): chrome.autofillPrivate.AddressEntry {
  const fullName = 'John Doe';
  const addressLines = patternMaker('xxxx Main St', 10);
  return {
    guid: makeGuid(),
    fields: [
      {type: FieldType.NAME_FULL, value: fullName},
      {type: FieldType.COMPANY_NAME, value: 'Google'},
      {
        type: FieldType.ADDRESS_HOME_STREET_ADDRESS,
        value: addressLines,
      },
      {type: FieldType.ADDRESS_HOME_STATE, value: 'CA'},
      {type: FieldType.ADDRESS_HOME_CITY, value: 'Venice'},
      {
        type: FieldType.ADDRESS_HOME_ZIP,
        value: patternMaker('xxxxx', 10),
      },
      {type: FieldType.ADDRESS_HOME_COUNTRY, value: 'US'},
      {
        type: FieldType.PHONE_HOME_WHOLE_NUMBER,
        value: patternMaker('(xxx) xxx-xxxx', 10),
      },
      {
        type: FieldType.EMAIL_ADDRESS,
        value: patternMaker('[email protected]', 16),
      },
    ],
    languageCode: 'EN-US',
    metadata: {
      isLocal: true,
      summaryLabel: fullName,
      summarySublabel: ', ' + addressLines,
    },
  };
}

/**
 * Creates a new empty credit card entry for testing.
 */
export function createEmptyCreditCardEntry():
    chrome.autofillPrivate.CreditCardEntry {
  const now = new Date();
  const expirationMonth = now.getMonth() + 1;
  return {
    expirationMonth: expirationMonth.toString(),
    expirationYear: now.getFullYear().toString(),
  };
}

/**
 * Creates a new random credit card entry for testing.
 */
export function createCreditCardEntry():
    chrome.autofillPrivate.CreditCardEntry {
  const cards = ['Visa', 'Mastercard', 'Discover', 'Card'];
  const card = cards[Math.floor(Math.random() * cards.length)];
  const cardNumber = appendLuhnCheckBit(patternMaker('xxxxxxxxxxxxxxx', 10));
  const now = new Date();
  return {
    guid: makeGuid(),
    name: 'Jane Doe',
    cardNumber: cardNumber,
    expirationMonth: Math.ceil(Math.random() * 11).toString(),
    expirationYear:
        (now.getFullYear() + Math.floor(Math.random() * 5) + 1).toString(),
    network: `${card}_network`,
    imageSrc: 'chrome://theme/IDR_AUTOFILL_CC_GENERIC',
    metadata: {
      isLocal: true,
      summaryLabel: card + ' ' +
          '****' + cardNumber.substr(-4),
      summarySublabel: 'Jane Doe',
    },
  };
}

/**
 * Creates a new valid IBAN entry for testing.
 * If `value` is not empty, set guid when creating the IBAN entry, otherwise,
 * leave it undefined, as it is an empty IBAN.
 */
export function createIbanEntry(
    value?: string, nickname?: string): chrome.autofillPrivate.IbanEntry {
  return {
    guid: value ? makeGuid() : undefined,
    value: (value || value === '') ? value : 'CR99 0000 0000 0000 8888 88',
    nickname: (nickname || nickname === '') ? nickname : 'My doctor\'s IBAN',
    metadata: {
      isLocal: true,
      summaryLabel: ibanPatternMaker(value || 'CR99 0000 0000 0000 8888 88'),
      summarySublabel: nickname || 'My doctor\'s IBAN',
    },
  };
}

/**
 * Creates a new random GUID for testing.
 */
export function makeGuid(): string {
  return patternMaker('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 16);
}

/**
 * Replaces any 'x' in a string with a random number of the base.
 * @param pattern The pattern that should be used as an input.
 * @param base The number base. ie: 16 for hex or 10 for decimal.
 */
function patternMaker(pattern: string, base: number): string {
  return pattern.replace(/x/g, function() {
    return Math.floor(Math.random() * base).toString(base);
  });
}

/**
 * Calculates and appends a Luhn check bit for the given card number.
 * https://en.wikipedia.org/wiki/Luhn_algorithm
 * @param cardNumber The card number to calculate a check bit for
 */
function appendLuhnCheckBit(cardNumber: string): string {
  const digitsInReverse = cardNumber.split('').reverse();
  let sum = 0;
  let doubleDigit = true;
  for (const digit of digitsInReverse) {
    let intDigit = Number(digit);
    assertFalse(Number.isNaN(intDigit));
    if (doubleDigit) {
      intDigit *= 2;
      sum += Math.floor(intDigit / 10) + (intDigit % 10);
    } else {
      sum += intDigit;
    }
    doubleDigit = !doubleDigit;
  }

  const checkDigit = (10 - (sum % 10)) % 10;
  return cardNumber + checkDigit.toString();
}

/**
 * Converts value (E.g., CH12 1234 1234 1234 1234) of IBAN to a partially masked
 * text formatted by the following rules:
 * 1. Reveal the first and the last four characters.
 * 2. Mask the remaining digits.
 * 3. The identifier string will be arranged in groups of four with a space
 *    between each group.
 * Examples: BE71 0961 2345 6769 will be shown as: BE71 **** **** 6769.
 */
function ibanPatternMaker(ibanValue: string): string {
  let output = '';
  const strippedValue = ibanValue.replace(/\s/g, '');
  for (let i = 0; i < strippedValue.length; ++i) {
    if (i % 4 === 0 && i > 0) {
      output += ' ';
    }
    if (i < 4 || i >= strippedValue.length - 4) {
      output += strippedValue.charAt(i);
    } else {
      output += `*`;
    }
  }
  return output;
}

/** Helper class to track AutofillManager expectations. */
export class AutofillManagerExpectations {
  requestedAddresses: number = 0;
  listeningAddresses: number = 0;
  removeAddress: number = 0;
}

/**
 * Test implementation
 */
export class TestAutofillManager extends TestBrowserProxy implements
    AutofillManagerProxy {
  data: {
    addresses: chrome.autofillPrivate.AddressEntry[],
    accountInfo: chrome.autofillPrivate.AccountInfo,
  };

  lastCallback:
      {setPersonalDataManagerListener: PersonalDataChangedListener|null};

  constructor() {
    super([
      'getAccountInfo',
      'getAddressList',
      'removeAddress',
      'removePersonalDataManagerListener',
      'setPersonalDataManagerListener',
      'setAutofillSyncToggleEnabled',
    ]);

    // Set these to have non-empty data.
    this.data = {
      addresses: [],
      accountInfo: {
        email: '[email protected]',
        isSyncEnabledForAutofillProfiles: true,
        isEligibleForAddressAccountStorage: false,
        isAutofillSyncToggleAvailable: false,
        isAutofillSyncToggleEnabled: false,
      },
    };

    // Holds the last callbacks so they can be called when needed.
    this.lastCallback = {
      setPersonalDataManagerListener: null,
    };
  }

  setPersonalDataManagerListener(listener: PersonalDataChangedListener) {
    this.methodCalled('setPersonalDataManagerListener');
    this.lastCallback.setPersonalDataManagerListener = listener;
  }

  removePersonalDataManagerListener(_listener: PersonalDataChangedListener) {
    this.methodCalled('removePersonalDataManagerListener');
  }

  getAccountInfo() {
    this.methodCalled('getAccountInfo');
    return Promise.resolve(this.data.accountInfo);
  }

  getAddressList() {
    this.methodCalled('getAddressList');
    return Promise.resolve(this.data.addresses);
  }

  saveAddress(_address: chrome.autofillPrivate.AddressEntry) {}

  removeAddress(_guid: string) {
    this.methodCalled('removeAddress');
  }

  setAutofillSyncToggleEnabled(_enabled: boolean) {
    this.methodCalled('setAutofillSyncToggleEnabled');
  }

  /**
   * Verifies expectations.
   */
  assertExpectations(expected: AutofillManagerExpectations) {
    assertEquals(
        expected.requestedAddresses, this.getCallCount('getAddressList'));
    assertEquals(
        expected.listeningAddresses,
        this.getCallCount('setPersonalDataManagerListener') -
            this.getCallCount('removePersonalDataManagerListener'));
    assertEquals(expected.removeAddress, this.getCallCount('removeAddress'));
  }
}

/** Helper class to track PaymentsManager expectations. */
export class PaymentsManagerExpectations {
  requestedCreditCards: number = 0;
  listeningCreditCards: number = 0;
  removedCreditCards: number = 0;
  addedVirtualCards: number = 0;
  requestedIbans: number = 0;
  removedIbans: number = 0;
  isValidIban: number = 0;
  authenticateUserAndFlipMandatoryAuthToggle: number = 0;
  getLocalCard: number = 0;
  bulkDeleteAllCvcs: number = 0;
}

/**
 * Test implementation
 */
export class TestPaymentsManager extends TestBrowserProxy implements
    PaymentsManagerProxy {
  private isValidIbanResult_: boolean = true;
  private isUserVerifyingPlatformAuthenticatorAvailable_: boolean|null = null;
  // <if expr="is_win or is_macosx">
  private isDeviceAuthAvailable_: boolean = false;
  // </if>

  data: {
    creditCards: chrome.autofillPrivate.CreditCardEntry[],
    ibans: chrome.autofillPrivate.IbanEntry[],
  };

  lastCallback:
      {setPersonalDataManagerListener: PersonalDataChangedListener|null};

  constructor() {
    super([
      'addVirtualCard',
      'authenticateUserAndFlipMandatoryAuthToggle',
      'bulkDeleteAllCvcs',
      'getCreditCardList',
      'getIbanList',
      'getLocalCard',
      'isValidIban',
      'removeCreditCard',
      'removeIban',
      'removePersonalDataManagerListener',
      'setPersonalDataManagerListener',
    ]);

    // Set these to have non-empty data.
    this.data = {
      creditCards: [],
      ibans: [],
    };

    // Holds the last callbacks so they can be called when needed.
    this.lastCallback = {
      setPersonalDataManagerListener: null,
    };
  }

  setPersonalDataManagerListener(listener: PersonalDataChangedListener) {
    this.methodCalled('setPersonalDataManagerListener');
    this.lastCallback.setPersonalDataManagerListener = listener;
  }

  removePersonalDataManagerListener(_listener: PersonalDataChangedListener) {
    this.methodCalled('removePersonalDataManagerListener');
  }

  getCreditCardList() {
    this.methodCalled('getCreditCardList');
    return Promise.resolve(this.data.creditCards);
  }

  logServerCardLinkClicked() {}

  logServerIbanLinkClicked() {}

  migrateCreditCards() {}

  removeCreditCard(_guid: string) {
    this.methodCalled('removeCreditCard');
  }

  saveCreditCard(_creditCard: chrome.autofillPrivate.CreditCardEntry) {}

  addVirtualCard(_cardId: string) {
    this.methodCalled('addVirtualCard');
  }

  removeVirtualCard(_cardId: string) {}

  saveIban(_iban: chrome.autofillPrivate.IbanEntry) {}

  removeIban(_guid: string) {
    this.methodCalled('removeIban');
  }

  getIbanList() {
    this.methodCalled('getIbanList');
    return Promise.resolve(this.data.ibans);
  }

  setIsValidIban(isValidIbanResult: boolean) {
    this.isValidIbanResult_ = isValidIbanResult;
  }

  isValidIban(_ibanValue: string) {
    this.methodCalled('isValidIban');
    return Promise.resolve(this.isValidIbanResult_);
  }

  setIsUserVerifyingPlatformAuthenticatorAvailable(available: boolean|null) {
    this.isUserVerifyingPlatformAuthenticatorAvailable_ = available;
  }

  isUserVerifyingPlatformAuthenticatorAvailable() {
    return Promise.resolve(this.isUserVerifyingPlatformAuthenticatorAvailable_);
  }

  authenticateUserAndFlipMandatoryAuthToggle() {
    this.methodCalled('authenticateUserAndFlipMandatoryAuthToggle');
  }

  getLocalCard(_guid: string) {
    this.methodCalled('getLocalCard');
    const card =
        this.data.creditCards.find(creditCard => creditCard.guid === _guid);
    if (card !== undefined) {
      return Promise.resolve(card);
    }
    return Promise.resolve(null);
  }

  // <if expr="is_win or is_macosx">
  setIsDeviceAuthAvailable(available: boolean) {
    this.isDeviceAuthAvailable_ = available;
  }

  checkIfDeviceAuthAvailable() {
    return Promise.resolve(this.isDeviceAuthAvailable_);
  }
  // </if>

  bulkDeleteAllCvcs() {
    this.methodCalled('bulkDeleteAllCvcs');
  }

  /**
   * Verifies expectations.
   */
  assertExpectations(expected: PaymentsManagerExpectations) {
    assertEquals(
        expected.requestedCreditCards, this.getCallCount('getCreditCardList'),
        'requestedCreditCards mismatch');
    assertEquals(
        expected.listeningCreditCards,
        this.getCallCount('setPersonalDataManagerListener') -
            this.getCallCount('removePersonalDataManagerListener'),
        'listeningCreditCards mismatch');
    assertEquals(
        expected.removedCreditCards, this.getCallCount('removeCreditCard'),
        'removedCreditCards mismatch');
    assertEquals(
        expected.addedVirtualCards, this.getCallCount('addVirtualCard'),
        'addedVirtualCards mismatch');
    assertEquals(
        expected.requestedIbans, this.getCallCount('getIbanList'),
        'requestedIbans mismatch');
    assertEquals(
        expected.removedIbans, this.getCallCount('removeIban'),
        'removedIbans mismatch');
    assertEquals(
        expected.authenticateUserAndFlipMandatoryAuthToggle,
        this.getCallCount('authenticateUserAndFlipMandatoryAuthToggle'),
        'authenticateUserAndFlipMandatoryAuthToggle mismatch');
    assertEquals(
        expected.getLocalCard, this.getCallCount('getLocalCard'),
        'getLocalCard mismatch');
    assertEquals(
        expected.bulkDeleteAllCvcs, this.getCallCount('bulkDeleteAllCvcs'),
        'bulkDeleteAllCvcs mismatch');
  }
}