tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

payment-request-constructor.https.sub.html (19819B)


      1 <!DOCTYPE html>
      2 <!-- Copyright © 2017 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). -->
      3 <meta charset="utf-8">
      4 <title>Test for PaymentRequest Constructor</title>
      5 <link rel="help" href="https://w3c.github.io/browser-payment-api/#constructor">
      6 <script src="/resources/testharness.js"></script>
      7 <script src="/resources/testharnessreport.js"></script>
      8 <script>
      9 "use strict";
     10 const testMethod = Object.freeze({
     11  supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
     12 });
     13 const defaultMethods = Object.freeze([testMethod]);
     14 const defaultAmount = Object.freeze({
     15  currency: "USD",
     16  value: "1.0",
     17 });
     18 const defaultNumberAmount = Object.freeze({
     19  currency: "USD",
     20  value: 1.0,
     21 });
     22 const defaultTotal = Object.freeze({
     23  label: "Default Total",
     24  amount: defaultAmount,
     25 });
     26 const defaultNumberTotal = Object.freeze({
     27  label: "Default Number Total",
     28  amount: defaultNumberAmount,
     29 });
     30 const defaultDetails = Object.freeze({
     31  total: defaultTotal,
     32  displayItems: [
     33    {
     34      label: "Default Display Item",
     35      amount: defaultAmount,
     36    },
     37  ],
     38 });
     39 const defaultNumberDetails = Object.freeze({
     40  total: defaultNumberTotal,
     41  displayItems: [
     42    {
     43      label: "Default Display Item",
     44      amount: defaultNumberAmount,
     45    },
     46  ],
     47 });
     48 
     49 // Avoid false positives, this should always pass
     50 function smokeTest() {
     51  new PaymentRequest(defaultMethods, defaultDetails);
     52  new PaymentRequest(defaultMethods, defaultNumberDetails);
     53 }
     54 test(() => {
     55  smokeTest();
     56  const request = new PaymentRequest(defaultMethods, defaultDetails);
     57  assert_true(Boolean(request.id), "must be some truthy value");
     58 }, "If details.id is missing, assign an identifier");
     59 
     60 test(() => {
     61  smokeTest();
     62  const request1 = new PaymentRequest(defaultMethods, defaultDetails);
     63  const request2 = new PaymentRequest(defaultMethods, defaultDetails);
     64  assert_not_equals(request1.id, request2.id, "UA generated ID must be unique");
     65  const seen = new Set();
     66  // Let's try creating lots of requests, and make sure they are all unique
     67  for (let i = 0; i < 1024; i++) {
     68    const request = new PaymentRequest(defaultMethods, defaultDetails);
     69    assert_false(
     70      seen.has(request.id),
     71      `UA generated ID must be unique, but got duplicate! (${request.id})`
     72    );
     73    seen.add(request.id);
     74  }
     75 }, "If details.id is missing, assign a unique identifier");
     76 
     77 test(() => {
     78  smokeTest();
     79  const newDetails = Object.assign({}, defaultDetails, { id: "test123" });
     80  const request1 = new PaymentRequest(defaultMethods, newDetails);
     81  const request2 = new PaymentRequest(defaultMethods, newDetails);
     82  assert_equals(request1.id, newDetails.id, `id must be ${newDetails.id}`);
     83  assert_equals(request2.id, newDetails.id, `id must be ${newDetails.id}`);
     84  assert_equals(request1.id, request2.id, "ids need to be the same");
     85 }, "If the same id is provided, then use it");
     86 
     87 test(() => {
     88  smokeTest();
     89  const newDetails = Object.assign({}, defaultDetails, {
     90    id: "".padStart(1024, "a"),
     91  });
     92  const request = new PaymentRequest(defaultMethods, newDetails);
     93  assert_equals(
     94    request.id,
     95    newDetails.id,
     96    `id must be provided value, even if very long and contain spaces`
     97  );
     98 }, "Use ids even if they are strange");
     99 
    100 test(() => {
    101  smokeTest();
    102  const request = new PaymentRequest(
    103    defaultMethods,
    104    Object.assign({}, defaultDetails, { id: "foo" })
    105  );
    106  assert_equals(request.id, "foo");
    107 }, "Use provided request ID");
    108 
    109 test(() => {
    110  smokeTest();
    111  assert_throws_js(TypeError, () => new PaymentRequest([], defaultDetails));
    112 }, "If the length of the methodData sequence is zero, then throw a TypeError");
    113 
    114 test(() => {
    115  smokeTest();
    116  const duplicateMethods = [
    117    {
    118      supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    119    },
    120    {
    121      supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    122    },
    123  ];
    124  assert_throws_js(RangeError, () => new PaymentRequest(duplicateMethods, defaultDetails));
    125 }, "If payment method is duplicate, then throw a RangeError");
    126 
    127 test(() => {
    128  smokeTest();
    129  const JSONSerializables = [[], { object: {} }];
    130  for (const data of JSONSerializables) {
    131    try {
    132      const methods = [
    133        {
    134          supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    135          data,
    136        },
    137      ];
    138      new PaymentRequest(methods, defaultDetails);
    139    } catch (err) {
    140      assert_unreached(
    141        `Unexpected error parsing stringifiable JSON: ${JSON.stringify(
    142          data
    143        )}: ${err.message}`
    144      );
    145    }
    146  }
    147 }, "Modifier method data must be JSON-serializable object");
    148 
    149 test(() => {
    150  smokeTest();
    151  const recursiveDictionary = {};
    152  recursiveDictionary.foo = recursiveDictionary;
    153  assert_throws_js(TypeError, () => {
    154    const methods = [
    155      {
    156        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    157        data: recursiveDictionary,
    158      },
    159    ];
    160    new PaymentRequest(methods, defaultDetails);
    161  });
    162  assert_throws_js(TypeError, () => {
    163    const methods = [
    164      {
    165        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    166        data: "a string",
    167      },
    168    ];
    169    new PaymentRequest(methods, defaultDetails);
    170  });
    171  assert_throws_js(
    172    TypeError,
    173    () => {
    174      const methods = [
    175        {
    176          supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    177          data: null,
    178        },
    179      ];
    180      new PaymentRequest(methods, defaultDetails);
    181    },
    182    "Even though null is JSON-serializable, it's not type 'Object' per ES spec"
    183  );
    184 }, "Rethrow any exceptions of JSON-serializing paymentMethod.data into a string");
    185 
    186 // process total
    187 const invalidAmounts = [
    188  "-",
    189  "notdigits",
    190  "ALSONOTDIGITS",
    191  "10.",
    192  ".99",
    193  "-10.",
    194  "-.99",
    195  "10-",
    196  "1-0",
    197  "1.0.0",
    198  "1/3",
    199  "",
    200  null,
    201  " 1.0  ",
    202  " 1.0 ",
    203  "1.0 ",
    204  "USD$1.0",
    205  "$1.0",
    206  {
    207    toString() {
    208      return " 1.0";
    209    },
    210  },
    211 ];
    212 const invalidTotalAmounts = invalidAmounts.concat([
    213  "-1",
    214  "-1.0",
    215  "-1.00",
    216  "-1000.000",
    217  -10,
    218 ]);
    219 test(() => {
    220  smokeTest();
    221  for (const invalidAmount of invalidTotalAmounts) {
    222    const invalidDetails = {
    223      total: {
    224        label: "",
    225        amount: {
    226          currency: "USD",
    227          value: invalidAmount,
    228        },
    229      },
    230    };
    231    assert_throws_js(
    232      TypeError,
    233      () => {
    234        new PaymentRequest(defaultMethods, invalidDetails);
    235      },
    236      `Expect TypeError when details.total.amount.value is ${invalidAmount}`
    237    );
    238  }
    239 }, `If details.total.amount.value is not a valid decimal monetary value, then throw a TypeError`);
    240 
    241 test(() => {
    242  smokeTest();
    243  for (const prop in ["displayItems", "shippingOptions", "modifiers"]) {
    244    try {
    245      const details = Object.assign({}, defaultDetails, { [prop]: [] });
    246      new PaymentRequest(defaultMethods, details);
    247      assert_unreached(`PaymentDetailsBase.${prop} can be zero length`);
    248    } catch (err) {}
    249  }
    250 }, `PaymentDetailsBase members can be 0 length`);
    251 
    252 test(() => {
    253  smokeTest();
    254  assert_throws_js(TypeError, () => {
    255    new PaymentRequest(defaultMethods, {
    256      total: {
    257        label: "",
    258        amount: {
    259          currency: "USD",
    260          value: "-1.00",
    261        },
    262      },
    263    });
    264  });
    265 }, "If the first character of details.total.amount.value is U+002D HYPHEN-MINUS, then throw a TypeError");
    266 
    267 test(() => {
    268  smokeTest();
    269  for (const invalidAmount of invalidAmounts) {
    270    const invalidDetails = {
    271      total: defaultAmount,
    272      displayItems: [
    273        {
    274          label: "",
    275          amount: {
    276            currency: "USD",
    277            value: invalidAmount,
    278          },
    279        },
    280      ],
    281    };
    282    assert_throws_js(
    283      TypeError,
    284      () => {
    285        new PaymentRequest(defaultMethods, invalidDetails);
    286      },
    287      `Expected TypeError when item.amount.value is "${invalidAmount}"`
    288    );
    289  }
    290 }, `For each item in details.displayItems: if item.amount.value is not a valid decimal monetary value, then throw a TypeError`);
    291 
    292 test(() => {
    293  smokeTest();
    294  try {
    295    new PaymentRequest(
    296      [
    297        {
    298          supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    299        },
    300      ],
    301      {
    302        total: defaultTotal,
    303        displayItems: [
    304          {
    305            label: "",
    306            amount: {
    307              currency: "USD",
    308              value: "-1000",
    309            },
    310          },
    311          {
    312            label: "",
    313            amount: {
    314              currency: "AUD",
    315              value: "-2000.00",
    316            },
    317          },
    318        ],
    319      }
    320    );
    321  } catch (err) {
    322    assert_unreached(
    323      `shouldn't throw when given a negative value: ${err.message}`
    324    );
    325  }
    326 }, "Negative values are allowed for displayItems.amount.value, irrespective of total amount");
    327 
    328 test(() => {
    329  smokeTest();
    330  const largeMoney = "1".repeat(510);
    331  try {
    332    new PaymentRequest(defaultMethods, {
    333      total: {
    334        label: "",
    335        amount: {
    336          currency: "USD",
    337          value: `${largeMoney}.${largeMoney}`,
    338        },
    339      },
    340      displayItems: [
    341        {
    342          label: "",
    343          amount: {
    344            currency: "USD",
    345            value: `-${largeMoney}`,
    346          },
    347        },
    348        {
    349          label: "",
    350          amount: {
    351            currency: "AUD",
    352            value: `-${largeMoney}.${largeMoney}`,
    353          },
    354        },
    355      ],
    356    });
    357  } catch (err) {
    358    assert_unreached(
    359      `shouldn't throw when given absurd monetary values: ${err.message}`
    360    );
    361  }
    362 }, "it handles high precision currency values without throwing");
    363 
    364 // Process shipping options:
    365 
    366 const defaultShippingOption = Object.freeze({
    367  id: "default",
    368  label: "",
    369  amount: defaultAmount,
    370  selected: false,
    371 });
    372 const defaultShippingOptions = Object.freeze([
    373  Object.assign({}, defaultShippingOption),
    374 ]);
    375 
    376 test(() => {
    377  smokeTest();
    378  for (const amount of invalidAmounts) {
    379    const invalidAmount = Object.assign({}, defaultAmount, {
    380      value: amount,
    381    });
    382    const invalidShippingOption = Object.assign({}, defaultShippingOption, {
    383      amount: invalidAmount,
    384    });
    385    const details = Object.assign({}, defaultDetails, {
    386      shippingOptions: [invalidShippingOption],
    387    });
    388    assert_throws_js(
    389      TypeError,
    390      () => {
    391        new PaymentRequest(defaultMethods, details, { requestShipping: true });
    392      },
    393      `Expected TypeError for option.amount.value: "${amount}"`
    394    );
    395  }
    396 }, `For each option in details.shippingOptions: if option.amount.value is not a valid decimal monetary value, then throw a TypeError`);
    397 
    398 test(() => {
    399  smokeTest();
    400  const shippingOptions = [defaultShippingOption];
    401  const details = Object.assign({}, defaultDetails, { shippingOptions });
    402  const request = new PaymentRequest(defaultMethods, details);
    403  assert_equals(
    404    request.shippingOption,
    405    null,
    406    "shippingOption must be null, as requestShipping is missing"
    407  );
    408  // defaultDetails lacks shipping options
    409  const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
    410    requestShipping: true,
    411  });
    412  assert_equals(
    413    request2.shippingOption,
    414    null,
    415    `request2.shippingOption must be null`
    416  );
    417 }, "If there is no selected shipping option, then PaymentRequest.shippingOption remains null");
    418 
    419 test(() => {
    420  smokeTest();
    421  const selectedOption = Object.assign({}, defaultShippingOption, {
    422    selected: true,
    423    id: "the-id",
    424  });
    425  const shippingOptions = [selectedOption];
    426  const details = Object.assign({}, defaultDetails, { shippingOptions });
    427  const requestNoShippingRequested1 = new PaymentRequest(
    428    defaultMethods,
    429    details
    430  );
    431  assert_equals(
    432    requestNoShippingRequested1.shippingOption,
    433    null,
    434    "Must be null when no shipping is requested (defaults to false)"
    435  );
    436  const requestNoShippingRequested2 = new PaymentRequest(
    437    defaultMethods,
    438    details,
    439    { requestShipping: false }
    440  );
    441  assert_equals(
    442    requestNoShippingRequested2.shippingOption,
    443    null,
    444    "Must be null when requestShipping is false"
    445  );
    446  const requestWithShipping = new PaymentRequest(defaultMethods, details, {
    447    requestShipping: "truthy value",
    448  });
    449  assert_equals(
    450    requestWithShipping.shippingOption,
    451    "the-id",
    452    "Selected option must be 'the-id'"
    453  );
    454 }, "If there is a selected shipping option, and requestShipping is set, then that option becomes synchronously selected");
    455 
    456 test(() => {
    457  smokeTest();
    458  const failOption1 = Object.assign({}, defaultShippingOption, {
    459    selected: true,
    460    id: "FAIL1",
    461  });
    462  const failOption2 = Object.assign({}, defaultShippingOption, {
    463    selected: false,
    464    id: "FAIL2",
    465  });
    466  const passOption = Object.assign({}, defaultShippingOption, {
    467    selected: true,
    468    id: "the-id",
    469  });
    470  const shippingOptions = [failOption1, failOption2, passOption];
    471  const details = Object.assign({}, defaultDetails, { shippingOptions });
    472  const requestNoShipping = new PaymentRequest(defaultMethods, details, {
    473    requestShipping: false,
    474  });
    475  assert_equals(
    476    requestNoShipping.shippingOption,
    477    null,
    478    "shippingOption must be null, as requestShipping is false"
    479  );
    480  const requestWithShipping = new PaymentRequest(defaultMethods, details, {
    481    requestShipping: true,
    482  });
    483  assert_equals(
    484    requestWithShipping.shippingOption,
    485    "the-id",
    486    "selected option must 'the-id"
    487  );
    488 }, "If requestShipping is set, and if there is a multiple selected shipping options, only the last is selected.");
    489 
    490 test(() => {
    491  smokeTest();
    492  const selectedOption = Object.assign({}, defaultShippingOption, {
    493    selected: true,
    494  });
    495  const unselectedOption = Object.assign({}, defaultShippingOption, {
    496    selected: false,
    497  });
    498  const shippingOptions = [selectedOption, unselectedOption];
    499  const details = Object.assign({}, defaultDetails, { shippingOptions });
    500  const requestNoShipping = new PaymentRequest(defaultMethods, details);
    501  assert_equals(
    502    requestNoShipping.shippingOption,
    503    null,
    504    "shippingOption must be null, because requestShipping is false"
    505  );
    506  assert_throws_js(
    507    TypeError,
    508    () => {
    509      new PaymentRequest(defaultMethods, details, { requestShipping: true });
    510    },
    511    "Expected to throw a TypeError because duplicate IDs"
    512  );
    513 }, "If there are any duplicate shipping option ids, and shipping is requested, then throw a TypeError");
    514 
    515 test(() => {
    516  smokeTest();
    517  const dupShipping1 = Object.assign({}, defaultShippingOption, {
    518    selected: true,
    519    id: "DUPLICATE",
    520    label: "Fail 1",
    521  });
    522  const dupShipping2 = Object.assign({}, defaultShippingOption, {
    523    selected: false,
    524    id: "DUPLICATE",
    525    label: "Fail 2",
    526  });
    527  const shippingOptions = [dupShipping1, defaultShippingOption, dupShipping2];
    528  const details = Object.assign({}, defaultDetails, { shippingOptions });
    529  const requestNoShipping = new PaymentRequest(defaultMethods, details);
    530  assert_equals(
    531    requestNoShipping.shippingOption,
    532    null,
    533    "shippingOption must be null, because requestShipping is false"
    534  );
    535  assert_throws_js(
    536    TypeError,
    537    () => {
    538      new PaymentRequest(defaultMethods, details, { requestShipping: true });
    539    },
    540    "Expected to throw a TypeError because duplicate IDs"
    541  );
    542 }, "Throw when there are duplicate shippingOption ids, even if other values are different");
    543 
    544 // Process payment details modifiers:
    545 test(() => {
    546  smokeTest();
    547  for (const invalidTotal of invalidTotalAmounts) {
    548    const invalidModifier = {
    549      supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    550      total: {
    551        label: "",
    552        amount: {
    553          currency: "USD",
    554          value: invalidTotal,
    555        },
    556      },
    557    };
    558    assert_throws_js(
    559      TypeError,
    560      () => {
    561        new PaymentRequest(defaultMethods, {
    562          modifiers: [invalidModifier],
    563          total: defaultTotal,
    564        });
    565      },
    566      `Expected TypeError for modifier.total.amount.value: "${invalidTotal}"`
    567    );
    568  }
    569 }, `Throw TypeError if modifier.total.amount.value is not a valid decimal monetary value`);
    570 
    571 test(() => {
    572  smokeTest();
    573  for (const invalidAmount of invalidAmounts) {
    574    const invalidModifier = {
    575      supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    576      total: defaultTotal,
    577      additionalDisplayItems: [
    578        {
    579          label: "",
    580          amount: {
    581            currency: "USD",
    582            value: invalidAmount,
    583          },
    584        },
    585      ],
    586    };
    587    assert_throws_js(
    588      TypeError,
    589      () => {
    590        new PaymentRequest(defaultMethods, {
    591          modifiers: [invalidModifier],
    592          total: defaultTotal,
    593        });
    594      },
    595      `Expected TypeError when given bogus modifier.additionalDisplayItems.amount of "${invalidModifier}"`
    596    );
    597  }
    598 }, `If amount.value of additionalDisplayItems is not a valid decimal monetary value, then throw a TypeError`);
    599 
    600 test(() => {
    601  smokeTest();
    602  const modifiedDetails = Object.assign({}, defaultDetails, {
    603    modifiers: [
    604      {
    605        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    606        data: ["some-data"],
    607      },
    608    ],
    609  });
    610  try {
    611    new PaymentRequest(defaultMethods, modifiedDetails);
    612  } catch (err) {
    613    assert_unreached(
    614      `Unexpected exception thrown when given a list: ${err.message}`
    615    );
    616  }
    617 }, "Modifier data must be JSON-serializable object (an Array in this case)");
    618 
    619 test(() => {
    620  smokeTest();
    621  const modifiedDetails = Object.assign({}, defaultDetails, {
    622    modifiers: [
    623      {
    624        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    625        data: {
    626          some: "data",
    627        },
    628      },
    629    ],
    630  });
    631  try {
    632    new PaymentRequest(defaultMethods, modifiedDetails);
    633  } catch (err) {
    634    assert_unreached(
    635      `shouldn't throw when given an object value: ${err.message}`
    636    );
    637  }
    638 }, "Modifier data must be JSON-serializable object (an Object in this case)");
    639 
    640 test(() => {
    641  smokeTest();
    642  const recursiveDictionary = {};
    643  recursiveDictionary.foo = recursiveDictionary;
    644  const modifiedDetails = Object.assign({}, defaultDetails, {
    645    modifiers: [
    646      {
    647        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    648        data: recursiveDictionary,
    649      },
    650    ],
    651  });
    652  assert_throws_js(TypeError, () => {
    653    new PaymentRequest(defaultMethods, modifiedDetails);
    654  });
    655 }, "Rethrow any exceptions of JSON-serializing modifier.data");
    656 
    657 //Setting ShippingType attribute during construction
    658 test(() => {
    659  smokeTest();
    660  assert_throws_js(TypeError, () => {
    661    new PaymentRequest(defaultMethods, defaultDetails, {
    662      shippingType: "invalid",
    663    });
    664  });
    665 }, "Shipping type should be valid");
    666 
    667 test(() => {
    668  smokeTest();
    669  const request = new PaymentRequest(defaultMethods, defaultDetails, {});
    670  assert_equals(request.shippingAddress, null, "must be null");
    671 }, "PaymentRequest.shippingAddress must initially be null");
    672 
    673 test(() => {
    674  smokeTest();
    675  const request1 = new PaymentRequest(defaultMethods, defaultDetails, {});
    676  assert_equals(request1.shippingType, null, "must be null");
    677  const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
    678    requestShipping: false,
    679  });
    680  assert_equals(request2.shippingType, null, "must be null");
    681 }, "If options.requestShipping is not set, then request.shippingType attribute is null.");
    682 
    683 test(() => {
    684  smokeTest();
    685  // option.shippingType defaults to 'shipping'
    686  const request1 = new PaymentRequest(defaultMethods, defaultDetails, {
    687    requestShipping: true,
    688  });
    689  assert_equals(request1.shippingType, "shipping", "must be shipping");
    690  const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
    691    requestShipping: true,
    692    shippingType: "delivery",
    693  });
    694  assert_equals(request2.shippingType, "delivery", "must be delivery");
    695 }, "If options.requestShipping is true, request.shippingType will be options.shippingType.");
    696 
    697 </script>